지난 주 스터디에서 나온 다른 한 가지 이슈는 "interface를 언제 생성하는 것이 적절한가?"이다. interface가 필요하다는 것은 다들 공감한다. 그렇다면 interface 생성을 어느 시점에 하는 것이 좋을까?
먼저 이 논의를 시작하게된 상황부터 공유해 보면 다음과 같다.
지난 주 스터디 이슈는 다음 코드를 리팩토링하는 것이었다.
public class UserService {
public User join(User user) throws SQLException, ExistedUserException {
UserDao userDao = new UserDao();
...
return user;
}
public User login(String userId, String password) throws SQLException, PasswordMismatchException {
UserDao userDao = new UserDao();
...
return user;
}
}
위 코드는 UserService가 UserDao 클래스를 직접 생성함으로써 UserDao에 강한 의존관계를 가지게 된다. 따라서 UserService를 테스트하려면 UserDao를 변경할 수 없기 때문에 데이터베이스 의존해서 테스트할 수 밖에 없다는 문제가 있다. 이 이슈를 해결하는 방법을 찾아보자는 것이 스터디의 주제였다.
클래스 다이어그램으로 살펴보면 다음과 같다.
이 문제를 해결하기 위해 스터디를 준비한 친구는 먼저 UserDao를 interface로 추출한 후 UserService에 UserDao를 Dependency Injection으로 구현하여 문제를 해결했다. 즉, 다음과 같은 구조로 리팩토링 했다.
테스트를 할 때는 MockUserDao를 활용하고, 실 서비스를 할 때는 JdbcUserDao를 사용하도록 한다. 이 단계에서 다음과 같은 이슈제기가 나왔다. 이 과정에서 반드시 interface를 생성해야 하느냐? 상속을 통해서 다음과 같이 구현하면 되지 않느냐?
즉, UserDao를 interface로 만들지 말고, 기존의 UserDao 클래스를 그대로 두고, UserDao를 상속하는 MockUserDao를 활용하면 되지 않느냐는 것이다.
이 같이 interface를 만드는 것에 대한 거부감은 스프링이 등장하면서 지식을 전파할 때 대부분의 책과 관련 문서에서 Service와 Dao에는 interface를 반드시 만드는 방식으로 예제 소스를 만들다보니 현재 대부분의 프로젝트에서 interface를 왜 만들어야 되는지도 모르는 상태에서 interface를 만드는 상황이 생기다 보니 발생한 것으로 판단된다. 내가 쓴 책에서도 똑같은 방식으로 예제가 구현되어 있다.
즉, 대부분의 프로젝트는 다음과 같은 구조로 구현한다.
위 그림과 같이 1:1로 interface와 class로 구현하는 방식이 대부분이기 때문에 interface의 필요성을 느끼지 못하는 상황이 발생한다. 오히려 interface에 대한 짜증과 거부감이 증가하는 현상이 발생할 수 밖에 없다. 특히 Test 코드도 만들지 않고, OOP에 대한 개념이나 설계도 없는 상황이라면 더더욱 그럴 것이다.
그렇다면 위와 같이 단위 테스트 코드를 만들고 MockUserDao를 만들고자 시점에는 interface를 생성해야 할까? 아니면 이슈제기한 바와 같이 상속을 통해 해결하는 것이 맞을까?
글 잘 읽고 덧글도 잘 읽었습니다. 저도 소견을 덧붙여봅니다.
1. 인터페이스를 만드는 시점
비슷한 의견입니다. 인터페이스가 필요한 이유를 스스로가 설명할 수 있고, 그로 인해 얻는 이득을 정확히 알 수 있을 때 도입하는 것이 좋다고 생각합니다. 단위 테스트를 위해서든, 구현체가 여러개가 생겨서이든 말이에요. 다만, 디자인 때문에 추가해야할 때라면 그 디자인을 통해 얻을 수 있는 것을 스스로가 정확하게 알고 설명할 수 있어야 한다고 생각합니다. 굉장히 추상적인 '디자인 좋기 때문에' 이런 사유는 가급적 피하는 게 좋을 것 같습니다.
2. 인터페이스 VS 상속
이 부분 역시 상황에 따라 틀리겠습니다만, 저는 위와 같은 정도 규모이면 상속을 선호할 것 같습니다. 제 경험상 제가 상속을 꺼렸던 이유를 생각해보면 부모가 너무 많은 상속구조 때문에 유지보수가 어려웠던 경험 때문이거나, Effective Java와 같은 곳에서 여러 근거로 상속을 사용하지 말라고 너무 강하게 얘기해서 였는데요. 실제로는 Spring처럼 상속에 상속에 상속에 상속이 되는 이런 유사한 구조가 아니라면 유지보수가 특별히 어렵지도 않고, Effective Java에서 우려하는 여러 문제점들도 실제 간단한 상속구조에서는 크게 문제가 안 된다고 생각합니다.
3. 엔지니어링
경험상 오버 엔지니어링은 개발할 때 비용도 증가시키지만 추후에 해당 엔지니어링이 가진 개념적 무게 등으로 인해 오히려 유지보수성이 떨어지는 경우가 많았던 것 같아요. 예를 들어 IF문으로 분기하면 되는 코드를 전략 패턴을 사용한다면 전략 패턴이 익숙하지 않은 사람에게는 IF문으로 작성 된 코드에 비해서 코드를 읽고 해석하는 데 더 시간이 들 것 같고요. 따라서 요즘 갖는 생각은 적절한 엔지니어링이 어쩌면 제일 좋은 디자인일지 모른다입니다. 위 예를 들어보면 인터페이스로 만들어놓는다면 추후에 여러 가지 상황에서 유연함을 좀더 확보할 수 있겠지만, 현재 시점 기준에서 보면 오버 엔지니어링일 수 있을 것 같고요. 이런 측면에서 보면 인터페이스는 일종의 투자라고 할 수 있을테고 유연함을 확보해놨다고 얘기할 수도 있을텐데요, 사실 이후에 생길 변화에 잘 맞는 유연함이라는 보장은 없다고 봅니다. 즉, 저는 이런 투자는 손해 볼 가능성이 높은 투자라고 생각합니다.
4. 동료에 대한 배려
제가 "Legacy 코드 작업하기"라는 과정에서 자주 얘기하는 주제인데요. 함께 일하는 동료를 생각해볼 때 좋은 디자인은 팀원의 대다수가 유지보수 할 때 어려움이 없는 디자인이라고 생각합니다. 예를 들어 IF문으로 되있는 코드를 Visitor 패턴을 적용해서 우아하게 바꾸어놓았어요. 그런데 팀원에 대부분이 예전 코드가 훨씬 읽고 수정하게 좋았다고 얘기한다면? 과연 나아진 것일까 생각해봅니다. 저는 최근에 팀원의 상황을 많이 고려하는 편이고, 팀원이 편하게 생각하는 디자인을 선호하는 편이에요. 물론 이런 방향은 긍정적인 방향으로 더 전진하는 것을 가로막을 수 있고, 스스로 인지하지 못하더라도 오히려 더 비효율적인 상황이 될 수도 있는 위험성이 있긴 하다고 생각합니다. 그럼에도 불구하고 이 부분을 생각하는 것은 너무 자주 좋은 디자인만 생각하다가 막상 함께 일하는 팀원을 미처 생각치 못하고, 그로 인해 많은 비효율이 발생할 수 있다고 생각하기 때문입니다.
간단히 쓰려고 했는데, 쓰다보니 많이 길어졌네요. 원래 글 쓸 때는 퇴고를 여러차례 하는 편인데, 그냥 한번도 수정없이 그냥 올릴게요. 혹시 설익은 부분 있어도 양해 부탁드립니다.
8개의 의견 from SLiPP
조건 1. refactoring 중이다. 2. Test를 위하여 MockUserDao를 만들어야 하는 요구사항이 있다.
이 조건으로 결론을 낸다면
즉, Test를 위해서건 실제로 Jdbc가 아닌, Hibernate, myBatis같은 ORM을 사용하든 요구명세가 바뀐것이기 때문에 그 시점에서 Interface가 필요하다(더불어 리펙토링중이므로)
상속보다는 인터페이스로 하는 것이 좋다고 봅니다. 인터페이스는 묘듈과 묘듈, 서비스와 서비스간의 경계를 명확하게 하는 효과와 결합도를 낮고 응집도를 높이고 Mock클래스를 인터페이스의 구현체로 생성해서 테스트를 쉽게 만드는 부수효과가 있습니다. (한마디로 굿아키텍처, 굿설계, 굿테스트)
단위테스트 코드를 작성할때 인터페이스를 먼저 만들고 구현체를 구현하거나 MockUserDao를 코딩하던 합니다.
저는 처음에는 인터페이스를 작성하지 않습니다. 구현체로 구현을 하고 필요한 시점에 인터페이스를 만드는데요. 위에 예시를 보고 말씀을 드리면, 1. UserService, UserDao를 작성한다. 2. test를 위해서 mock framework를 사용하면 구현체를 유지한다.(요즘 mock framework는 구현체를 mock으로 만들 수 있다) 3. 단, 테스트를 위해서 MockUserDao가 반드시 필요한 경우는 인터페이스로 만든다. UserDao, JdbcUserDaoImpl, MockUserDaoImpl
인터페이스를 만드는 조건은 단 하나입니다. 필요한 시점에 만든다.
감사합니다.
1. 인터페이스는 어느시점에 생성하는 것이 적절한가?
달걀이 먼저냐? 닭이 먼저냐?의 문제인가요.ㅎㅎ 설계시점에는 인터페이스를 먼저 정의하여 전체적인 레이아웃을 잡는편인데요. 코딩시점에는 굳이 인터페이스가 당장 필요하지 않다면 구현체를 먼저 만들고 나중에 인터페이스로 추출/추상화하기도 합니다. 항상 코딩하면서 리팩토링하다보면 설계도 바뀌고 그에 맞게 새로운 인터페이스도 도출되고 그러다보면 가끔 디자인패턴이 적용되어 있기도하고.. 개발을 잘하고 있는건지 모르겠네요.;
2. 그렇다면 위와 같이 단위 테스트 코드를 만들고 MockUserDao를 만들고자 시점에는 interface를 생성해야 할까? 아니면 이슈제기한 바와 같이 상속을 통해 해결하는 것이 맞을까?
인터페이스의 기본적인 역할은 명세와 구현의 분리입니다. 그래서 다형성(polymorphism)이란 특성을 가지고 있지요. 이런 다형성은 상속을 통한 부모/자식 클래스간에도 있는데요.
그럼 Dependency Injection에서 상속 관계가 아닌 인터페이스를 이용하는 이유는 무엇일까요? DI는 의존성을 제거하여 테스트를 쉽게하고 구현체의 변경에 용이하게 함인 걸로 알고 있는데요.
(제가 생각하기에) 이미 상속이란 것이 하나의 구현체를 물려 받은 것이니 다른객체와 의존성이 있을 가능성이 크지 않을까요? DI의 목적이 의존성을 제거하기 위함인데 의존성이 늘어나는 꼴이니 인터페이스를 사용한게 아닐까요? 인터페이스 자체는 명세이다보니 다른객체와의 직접적인 의존성은 있을 수 없으니깐요.
그럼 MockUserDao를 interface를 써서 생성해야할까요? 아니면 상속을 통해 해결해야할까요? 일반적으로 위와 같은 이유로 interface가 더 좋지않을까합니다. 상속을 통해 구현한다면 부모클래스의 의존성 문제도 해결해야되니깐요. 예를 들면 부모클래스의 변경사항으로 테스트가 실패할 수도 있고요. 물론 의존관계가 없다면 상속받아 테스트하고자하는 메소드만 stub이나 fake object로 override하는 것도 방법일 것 같습니다. (물론 전제는 Mocking framework를 사용하지 않는다면;)
잠이안와 답글달다가 횡성수설하고 가네요.^^;
@ologist 나도 mock framework를 사용하는 경우 인터페이스를 만들지 않고 테스트가 가능하기 때문에 인터페이스를 만들지 않는 경우가 있다. 위 본 글과 같이 MockUserDao를 구현하는 방식으로 테스트를 진행한다면 인터페이스로 추출하는 것이 타당하다고 생각되지만 MockUserDao를 만들지 않고 mock framework를 사용할 때는 인터페이스를 만들어 봤자 인터페이스 : 클래스가 1:1이 되는 경우가 많기 때문에...
나도 1:1이 발생하는 상황이 많아 인터페이스를 굳이 만들지 않는데 "가끔은 정말 이렇게 구현하는 것이 맞나?"라는 의구심이 들 때도 있다. 이렇게 구현할 경우 내가 무엇인가 놓치는 상황이 발생하는 것은 아닐까라는 생각..
@lark 접근 방식에 공감합니다. 저도 비슷한 방식으로 개발합니다.
상속과 관련해서는 구현의 편의성이 있을지는 모르지만 MockUserDao가 UserDao에 의존하고 있으며, UserDao의 변경으로 인해 잘 동작하던 단위 테스트가 깨지는 상황이 발생할 수 있다고 생각합니다. OOP는 특정 클래스의 변경이 다른 부분에 영향을 미치지 않도록 구현해야 되는데 위와 같이 상속을 통한 구현은 UserDao의 변경이 다른 부분에 영향을 미치기 때문에 상속은 좋은 선택이 아니라고 생각됩니다.
인터페이스의 필요성은 느끼겠는데 실제로 프로그래밍할 때 인터페이스를 추출하는게 쉽지 않더라고요. 참 많은 연습과 경험이 필요하다는 생각이 듭니다. 물론 그래서 더 재미있는 영역이 소프트웨어 개발이겠죠. 정답이 없기 때문에...
글 잘 읽고 덧글도 잘 읽었습니다. 저도 소견을 덧붙여봅니다.
1. 인터페이스를 만드는 시점
비슷한 의견입니다. 인터페이스가 필요한 이유를 스스로가 설명할 수 있고, 그로 인해 얻는 이득을 정확히 알 수 있을 때 도입하는 것이 좋다고 생각합니다. 단위 테스트를 위해서든, 구현체가 여러개가 생겨서이든 말이에요. 다만, 디자인 때문에 추가해야할 때라면 그 디자인을 통해 얻을 수 있는 것을 스스로가 정확하게 알고 설명할 수 있어야 한다고 생각합니다. 굉장히 추상적인 '디자인 좋기 때문에' 이런 사유는 가급적 피하는 게 좋을 것 같습니다.
2. 인터페이스 VS 상속
이 부분 역시 상황에 따라 틀리겠습니다만, 저는 위와 같은 정도 규모이면 상속을 선호할 것 같습니다. 제 경험상 제가 상속을 꺼렸던 이유를 생각해보면 부모가 너무 많은 상속구조 때문에 유지보수가 어려웠던 경험 때문이거나, Effective Java와 같은 곳에서 여러 근거로 상속을 사용하지 말라고 너무 강하게 얘기해서 였는데요. 실제로는 Spring처럼 상속에 상속에 상속에 상속이 되는 이런 유사한 구조가 아니라면 유지보수가 특별히 어렵지도 않고, Effective Java에서 우려하는 여러 문제점들도 실제 간단한 상속구조에서는 크게 문제가 안 된다고 생각합니다.
3. 엔지니어링
경험상 오버 엔지니어링은 개발할 때 비용도 증가시키지만 추후에 해당 엔지니어링이 가진 개념적 무게 등으로 인해 오히려 유지보수성이 떨어지는 경우가 많았던 것 같아요. 예를 들어 IF문으로 분기하면 되는 코드를 전략 패턴을 사용한다면 전략 패턴이 익숙하지 않은 사람에게는 IF문으로 작성 된 코드에 비해서 코드를 읽고 해석하는 데 더 시간이 들 것 같고요. 따라서 요즘 갖는 생각은 적절한 엔지니어링이 어쩌면 제일 좋은 디자인일지 모른다입니다. 위 예를 들어보면 인터페이스로 만들어놓는다면 추후에 여러 가지 상황에서 유연함을 좀더 확보할 수 있겠지만, 현재 시점 기준에서 보면 오버 엔지니어링일 수 있을 것 같고요. 이런 측면에서 보면 인터페이스는 일종의 투자라고 할 수 있을테고 유연함을 확보해놨다고 얘기할 수도 있을텐데요, 사실 이후에 생길 변화에 잘 맞는 유연함이라는 보장은 없다고 봅니다. 즉, 저는 이런 투자는 손해 볼 가능성이 높은 투자라고 생각합니다.
4. 동료에 대한 배려
제가 "Legacy 코드 작업하기"라는 과정에서 자주 얘기하는 주제인데요. 함께 일하는 동료를 생각해볼 때 좋은 디자인은 팀원의 대다수가 유지보수 할 때 어려움이 없는 디자인이라고 생각합니다. 예를 들어 IF문으로 되있는 코드를 Visitor 패턴을 적용해서 우아하게 바꾸어놓았어요. 그런데 팀원에 대부분이 예전 코드가 훨씬 읽고 수정하게 좋았다고 얘기한다면? 과연 나아진 것일까 생각해봅니다. 저는 최근에 팀원의 상황을 많이 고려하는 편이고, 팀원이 편하게 생각하는 디자인을 선호하는 편이에요. 물론 이런 방향은 긍정적인 방향으로 더 전진하는 것을 가로막을 수 있고, 스스로 인지하지 못하더라도 오히려 더 비효율적인 상황이 될 수도 있는 위험성이 있긴 하다고 생각합니다. 그럼에도 불구하고 이 부분을 생각하는 것은 너무 자주 좋은 디자인만 생각하다가 막상 함께 일하는 팀원을 미처 생각치 못하고, 그로 인해 많은 비효율이 발생할 수 있다고 생각하기 때문입니다.
간단히 쓰려고 했는데, 쓰다보니 많이 길어졌네요. 원래 글 쓸 때는 퇴고를 여러차례 하는 편인데, 그냥 한번도 수정없이 그냥 올릴게요. 혹시 설익은 부분 있어도 양해 부탁드립니다.
@자바지기 정답이 없다는 말씀이 와닿네요.ㅎㅎ 그래서 이런 토론도 재미진 것 같아요
@Min Cha 엔지니어링 & 동료에 대한 배려 부분에 깊이 공감합니다. 디자인 패턴의 적용 또한 확장이 필요한 시점에 복잡도를 낮추기 위함이어야지 이득을 명확히 알 수 없는 흉내내기식 적용은 오히려 복잡도만 높이고 코드의 가독성을 떨어 뜨리는 것 같아요. 또 함축적인 코드도 간결해 보일 수 있지만 코드의 가독성을 떨어뜨릴 수 있겠고요.
그러고보니 모든게 case by case인 것 같네요. 결국 당연한 이야기지만 다른사람이 내 코드를 이해하는데 걸리는 시간을 최소화하는게 가장 중요하지 않을까 싶습니다.
p.s 엇 차장님 Legacy 코드 작업하기 명강의는 작년에 잘들었습니다! :)
의견을 남기기 위해서는 SLiPP 계정이 필요합니다.
안심하세요! 회원가입/로그인 후에도 작성하시던 내용은 안전하게 보존됩니다.
SLiPP 계정으로 로그인하세요.
또는, SNS 계정으로 로그인하세요.