우아한테크코스 지하철 미션을 수행하면서, 내 영속성 계층에서 조회한 자원으로 "지하철 호선"과 "호선에 속한 역의 정보", 그리고 "각 역 사이의 거리" 정보를 모두 가져와 "Line"이라는 객체를 만들어야 하는 순간이 있었다.
초기 Service 클래스의 형태는 대충 다음과 같았었다.
* 대충 흐름만 보여주는 코드라 비약이 많다. 그냥 Dao를 의존하는 것에만 집중하자
@Service
public class LineService {
private final LineDao lineDao;
private final SectionDao sectionDao;
private final StationDao stationDao;
///생성자 생략
...
public LineResponse findLine(Long 호선번호){
호선이름 = lineDao.selectById(호선번호);
구간정보들 = sectionDao.selectByLineId(호선번호)
호선소속역들 = stationDao.selectByLineId(호선번호);
Line line = new Line(호선이름,구간정보들,호선소속역들);
return LineResponse.Of(line);
}
...
}
문제는 이렇게 코드를 나니 Dao에 서비스 로직이 큰 의존을 하게 되었다. 지금은 단순히 조회를 하는 것에 지나지 않지만, 저장, 삭제, 수정 등등...여러 가지 기능을 추가할 때마다. 역시 의존하고 있는 여러 Dao의 메소드를 연달아서 계속 호출한다. 서비스의 책임은 점점 무거워지고, 로직은 복잡해지며, 코드 길이도 계속 늘어난다.
SRP를 지키지 못하게 되고 객체지향에서 점점 멀어지게 되었다. 또 Dao의 로직이 변경될 때, 도메인에서 영향을 받을 위험성이 매우 커졌다.
위와 맥락이 비슷하다. 지구상의 모든 비즈니스 요구사항이 자원을 반드시 영속성에서만 호출해 가져올까? 물론 지금 미션에야 JdbcTemplate만을 사용해 영속성에서만 자원을 가져오고 있지만, 만약 다른 곳에서도 자원을 가져와야 하는 요구사항이 생긴다면? 예를 들어서 RestTemplate이나 WebClient를 사용하여 api call을 통해 다른 서버에서 자원을 가져오는 경우도 생긴다면? 지금과 같은 형태를 가질 경우 또 이를 의존하는 객체를 만들고, 서비스에 주입시켜야 한다.
도메인 계층이 원하는 자원의 형태는 정해져 있는데(여기서는 Line과 같다), 그 자원을 가져오기 위한 방법이 자꾸 추가/삭제된다면 이 경우 도메인 계층의 코드는 계속 변경된다.
@Repository
public interface LineRepository {
Line findLineById(Long id);
}
@Repository
public class JdbcLineRepository implements LineRepository {
private final LineDao lineDao;
private final SectionDao sectionDao;
private final StationDao stationDao;
///생성자 생략
@Override
public Line findLineById(final Long id) {
호선이름 = lineDao.selectById(호선번호);
구간정보들 = sectionDao.selectByLineId(호선번호);
호선소속역들 = stationDao.selectByLineId(호선번호);
return new Line(호선이름,구간정보들,호선소속역들);
}
}
다음과 같은 LineRepository 인터페이스를 선언하고, 구현체인 JdbcLineRepository가 dao를 호출해서 자원을 가져온 후, Line 객체에 맞게 이를 조립한다.
아까 언급했던 서비스의 책임을 기억하는가?
위에서 1번 2번의 책임을 이곳으로 넘긴다.
@Service
public class LineService {
private final LineRepository lineRepository;
///생성자 생략
...
public LineResponse findLine(Long 호선번호){
Line line = lineRepository.findlineById(호선번호);
return LineResponse.Of(line)
}
...
}
이제 서비스는 3번의 책임, 즉 비즈니스 로직을 적용하는 책임만 갖게 된다. 이제 서비스는 자원을 어떤 곳에서 가져오든, 가져온 자원을 어떻게 도메인 객체로 바꿔 조립하는지는 알 수가 없다. 아니 정확히 말하면 알빠가 없다. 그냥 서비스가 비즈니스 로직을 적용할 도메인 객체가 들어오기만 하면 된다. Repository의 인터페이스만 바라보고 내가 원하는 객체를 내놓으라고 요청만 하면 된다.
이제 어디서 자원을 가져오든지에 대해 도메인 계층은 관심이 없다. Repository의 구현체만 만들어서 갈아 끼운다. RestTemplate를 통해 가져오면 RestLineRepository를 구현체로 만들어 갖다 넣는다. 구현체에서 열심히 객체로 조립해 서비스에서 원하는 객체로 넘겨주기만 하면 된다. 이 때 Repository 인터페이스에 의존하는 도메인 계층에서는 코드가 변화할 가능성은 크지 않다.
결론적으로 Repository를 사용하면서 Dip를 잘 적용한 아키텍처가 만들어졌다고 생각한다. 변화하기 쉬운 것에 의존하지 않으니, 다른 클래스의 변경에 영향을 받을 일이 줄어든다. 그러므로 도메인은 도메인 로직에만 집중할 수 있다. 조립된 객체에 비즈니스 로직만 적용하면 된다는 책임이 명확하다. 책임이 나눠지면 해결 가능한 문제의 단위가 작아지고, 작은 단위의 문제만 해결하면 되므로 코드의 유지보수성이 증가된다.