현재 패키지 구조
현재 진행 중인 GoodsForYou 중고 거래 애플리케이션에서는 패키지 구조의 큰 틀을, 기능 단위의 패키지 구조 하위에 패키지구조를 구성하는 방식으로 진행하였습니다.
이러한 구조를 유지함으로써 해당 기능 패키지 별로 응집도를 높일 수 있었습니다.
또한 한 기능 패키지에서 레이어드 아키텍처를 적용함에 따라, Presentation은 Presentation 끼리, Application은 Application 끼리 묶는 방식으로, 레이어드 아키텍처를 도입해, 각각의 레이어 별로 관심사를 분리하고, 패키지 간의 의존성의 방향을 단방향으로 제한하였습니다.
각 계층별 설명
- Presentation : 사용자와 상호작용을 하고, 브라우저 통신 로직을 처리하는 레이어입니다.
- Application : 애플리케이션 계층은 사용자의 요청과 관련된 굿즈 포유 서비스 내에서 정해진 특정 비즈니스 규칙을 실행하는 역할을 담당합니다.
- Domain : 애플리케이션의 중심과 같은 부분입니다. 수정이 빈번하게 일어나지 않는 클래스들이 존재하는 계층입니다.
- Infrastructure : Database 서버나 Message Queue와 같은 외부와 통신을 하여 작동을 하는 클래스들이 모여있는 계층입니다.
이렇게 패키지 구조에서 각 계층을 나누어 관리함으로써, 앞서 설명한 각 계층별의 관심사를 구분함으로써, 특정 계층 내의 컴포넌트는 해당 계층과 관련된 로직만 처리할 수 있게 됩니다.
포트와 어댑터 패턴
현재 진행 중인 프로젝트는 Infrastructure 레이어의 저장소 역할을 하는 클래스가 초기에는 자바 HashMap기반의 MemoryDB였었다면, 지금은 MyBatis를 이용해 실제 DB와 연동을 하는 방식으로 변경이 되었습니다. 이렇게 저장소가 변경될 때마다, 해당 저장소 역할을 하는 코드의 수정과 함께 애플리케이션 레이어에 속해, Infrastructure Layer에 속한 저장소를 사용하는 코드의 변경도 일어나게 되는 걸 알게 되었습니다.
이를 통해 어떻게 해야 외부 환경의 변화에도, 애플리케이션 레이어나 도메인 레이어의 코드는 변하지 않도록 유지할 수 있을까?(SOLID 법칙 중 OCP를 준수하는 작성 하고자 생각했습니다.)라고 생각했습니다. 그중 알게 된 패턴 중 하나 인 포트와 어댑터 아키텍처를 사용해 보기로 했습니다.
포트와 어댑터
헥사고날 아키텍처(Hexagonal Architecture)로 더 잘 알려져 있는 포트와 어댑터 아키텍처는 인터페이스나 기반 요소(Infrastructure)의 변경에 영향을 받지 않는 핵심 코드를 만들고 이를 견고하게 관리하는 것이 목표입니다.
그렇다면 포트와 어댑터에 대해 알아보도록 하겠습니다.
현재 프로젝트는 Java를 사용하고 있기 때문에, Java를 기준으로 설명드리겠습니다. 클래스 메서드의 시그니처나 Java에서의 Interface가 바로 포트라고 할 수 있습니다. 다음으로 어댑터는, 디자인 패턴에도 있듯이 클라이언트에 제공해야 할 인터페이스를 따르면서도 내부 구현은 서버의 인터페이스로 위임하는 것입니다.
현재 진행 중인 프로젝트의 코드를 통해 더 자세히 알아보겠습니다.
굿즈 포유 서비스에서 유저의 정보를 관리하는 코드입니다.
public class MemoryUserRepositoryAdapter implements UserRepositoryPort {
private static Map<Long, User> userStore = new ConcurrentHashMap<>();
private static AtomicLong userIdSequence = new AtomicLong();
public void save(User user) {
long currentId = userIdSequence.incrementAndGet();
userStore.put(currentId, user);
}
public User findByName(String name) {
return userStore.values().stream()
.filter(user -> user.name().equals(name))
.findFirst()
.orElse(null);
}
public User findByEmail(String email) {
return userStore.values().stream()
.filter(user -> user.email().equals(email))
.findFirst()
.orElse(null);
}
}
MemoryUserRepositoryAdapter는 사용자의 정보를 MemoryDB에 저장하는 저장소의 역할을 하는 코드입니다. 그리고 Adapter라는 이름을 붙여 명시적으로 어댑터 패턴을 적용했다는 것을 보여주고 있습니다.
public interface UserRepositoryPort {
void save(User user);
User findByName(String name);
User findByEmail(String email);
}
UserRepositoryPort는 자바에서 제공하는 interface를 이용해, 각각의 save() , findByName() , findByEmail() 메서드의 구현을 MemoryUserRepositoryAdapter와 같이 UserRepositoryPort를 구현한 구현체들한테 위임하고 있습니다.
@Component
public class NewUserCreator implements UserCreator {
private final UserCreatorPort userRepositoryPort;
public NewUserCreator(UserRepositoryPort userRepositoryPort) {
this.userRepositoryPort = userRepositoryPort;
}
@Override
public void save(User user) {
userRepositoryPort.save(user);
}
}
NewUserCreator는 유저의 정보를 저장해 주는 역할을 갖고 있는 객체입니다. 위 코드에서 알 수 있듯이, UserCreatorPort를 주입받음 으로써, 저장소 역할을 하는 객체의 실제 코드의 내부 구현을 알 필요가 없이, UserCreatorPort 인터페이스에서 제공해 주는 save()라는 메시지 만을 통해 객체들이 협력하고 있음을 알 수 있습니다. 이렇게 함으로써, 만약 MemoryUserRepository에서, MybatisUserRepository로 변경되고, JpaUserRepository로 변경되고, 또한 추후에 관계형 데이터베이스가 아닌 Nosql을 사용하게 되더라도 변경 시에, 어댑터에 해당하는 코드들만 수정해 주면 되기 때문에, OCP를 준수하는 코드를 작성할 수 있었습니다.
포트와 어댑터 패턴을 적용하지 않았을 때
위 그림처럼 포트와 어댑터 패턴을 적용하지 않았을 때, UserService라는 객체는 MemoryUserRepository라는 객체를 직접적으로 알고 있고 그 내부구현까지 알고 있기 때문에, 추후 MemoryUserRepository에서 MybatisUserRepository와 같이 저장소 역할을 하는 객체를 변경하려 할 때, UserService까지 변경을 해줘야 하는 일이 생기게 될 것입니다.
한마디로 말해 OCP를 준수하지 못한 코드를 작성하게 됩니다.
포트와 어댑터 패턴 적용
반면에 위 그림은 포트와 어댑터 패턴을 적용해, UserService에서 UserRepositoryPort라는 인터페이스를 통해, 유저를 저장하는 방식의 변경이 생기더라도, 그 내부구현을 알지 못하고 인터페이스를 통해 메시지를 주고받으면서 객체 간의 협력이 형성되고 있기 때문에, UserService 객체와 UserRepositoryPort 객체간의 강한 의존성을 끊어 낼 수 있었습니다. 또한 UserRepositoryPort의 변경에도 UserService는 영향받지 않는 OCP를 준수하는 코드를 작성할 수 있었습니다.
참고
https://engineering.linecorp.com/ko/blog/port-and-adapter-architecture/
https://www.baeldung.com/cs/layered-architecture
https://www.oreilly.com/library/view/software-architecture-patterns/9781491971437/ch01.html
'프로그래밍 > 프로젝트' 카테고리의 다른 글
도커 컴포즈 사용 시 DB 초기화 문제 해결 과정 (0) | 2023.03.18 |
---|---|
캐싱은 언제 적용하는게 좋을까? (2) | 2023.03.07 |
CAP 이론을 바탕으로 NoSQL 을 적용 할 만한 포인트 고려 (0) | 2023.03.02 |
인증 방식으로 세션 VS 토큰 어떤걸 선택해야 할까? (0) | 2023.01.17 |
다중 서버 환경에서 사용자 로그인 구현시 세션 관리 방법으로 어떤걸 선택해야할까? (0) | 2023.01.16 |