📂 목차
📚 본문
미니프로젝트(도서 서비스, 멤버 서비스) 구현에 있어 겪었던 문제점과 해결책들을 적어보려고 한다.
내가 느꼈던 혼란을 좀 정리하여서 다음에는 의미가 분명하도록 하기 위함이다.
SOLID
Single Responsibility Principle
클래스 또는 모듈은 하나의 책임만을 가져야 하며,
변경 사유가 여러 가지가 되면 안된다.
MemberService 는 회원 가입/탈퇴/조회만 하고,
책 관리는 LibraryService 가 하도록
나는 위를 Service 계층에서 적용시키지 못하여 추후에 수정하려고 한다.
Open-Closed Principle
확장에는 열려있고, 수정에는 닫혀 있어야 한다.
새로운 기능을 추가할 때에는 기존 코드를 직접 수정하지 않고도 확장으로 해결 할 수 있도록 한다.
전략 패턴은 할인 정책을 바꾸려면 DiscountPolicy 인터페이스
구현체만 추가 하여 새로운 기능을 추가할 수 있을 것이다.
Liskov Substitution Principle
상위 타입 객체를 하위 타입으로 바꿔도 프로그램 동작이 깨지면 안된다.
즉 자식 클래스는 부모 클래스의 계약(기능)을 위반하지 않아야 한다.
Rectangle 의 자식이 Square 라면, setWidth() 와 setHeight() 동작이 부모의 기대를 충족해야 한다.
즉 상위 동작이 하위 동작을 포함시키냐는 것이다.
Interface Segregation Principle
클라이언트는 자신이 사용하지 않는 기능에 의존적이지 않아야 한다.
너무 많은 기능을 가진 인터페이스를 나눠서 꼭 필요한 것만 구현하도록 한다.
Printer 인터페이스는 print(), scan(), fax() 를 가지게 된다면 단순히 프린터만 하는 PurePrinter 는 위 세 개를 다 구현해야 한다.
Dependency Inversion Principle
의존관계를 맺을 때 변화하는 클래스와 관계를 맺기 보다 변화하지 않는 클래스와 관계를 맺는 것.
패키지 혼동
패키지 의미가 분명하지 않아 나도 클래스를 여기저기 의미가 이거겠지 하고 유추하며
분배했던 기억이 나서 이를 확실히 하기 위해 다음처럼 나눈다.
VO 패키지
Value Object의 약자이며 멋사를 진행할 때 처음 보았던 패키지이다.
- Equality 를 가지고 이 동등성을 식별자가 아니라 값 그 자체로 비교하는 객체
- Immutable 로 설계하는 것이 바람직하며, 두 VO 는 필드 값이 같으면 같은 객체로 취급되게 된다.
- equals/hashCode 오버라이드가 중요하다.
내가 구현했던 Book
클래스는 record
를 사용해서 선언했으며 이는 3번의 수고를 덜 수 있어 괜찮았다.
좋은 점은 불변이라서 값이 안변하여 로직에서 어떤 값이 들어가 있을지 예상이 간다는 것이다.
Domain 패키지
비즈니스 로직과 규칙과 함께 핵심적인 계층이다.
포함되는 것
Entity
패키지VO
패키지Domain Service
(특정 엔티티에 귀속되지 않는 도메인 로직)Aggregate
,Repository
인터페이스
가 여기에 들어간다고 한다. 특히, 외부 기술에 의존하지 않아야 하며(POJO),
시스템이 해결하는 문제의 본질적인 개념을 모델링한 공간이라고 한다.
내가 틀렸던 것은 Domain 을 Entity 와 혼동하고 있었기 때문이다.
조금 더 국소적인 개념으로 바라본게 의사소통의 문제가 생겼던거 같다.
Entity 패키지
식별자(ID)를 가지며, 라이프 사이클이 관리되는 객체이다.
- 같은 ID = 같은 객체
- DB 테이블과 1:1 매핑이 됨(JPA @Entity)
- 가변적(mutable) 일 수 있음
Data 패키지
데이터 전송, 저장 계층이며, DB, API 등 외부 저장소와의 상호작용을 담당하는 계층이다.
포함되는 것
- Repository 구현체 (JPA Repository)
- DAO (Data Access Object) 패키지
- DTO (Data Transfrer Object) 패키지
특징으로는 아까와는 다르게 Repository 가 외부 라이브러리에 의존적임을 볼 수 있으며,
domain 을 그대로 노출하지 않고, data 레이어에서 직접 변환 후 전달하는게 특징이다.
구조 아키텍쳐
MVC 아키텍처
- Model: 핵심 데이터와 비즈니스 로직을 담당한다.
DB 와 연결된 엔티티, 서비스 로직 등이 들어간다. - View: 사용자에게 보여질 화면이다.
- Controller: 사용자의 요청을 받아 Model 을 조작하고 결과를 View 에 전달한다.
대규모 프로젝트에서는 맞지 않다. Controller 가 비대해지게 되고, 서비스/도메인 계층이 부족하면 Model 과 Controller 간의 경계가 모호해진다.
Domain-Driven Design 아키텍처
비즈니스 로직을 중심으로 설계하는 아키텍쳐이다.
- Domain: 엔티티, VO, 도메인 서비스 등등
- Application: 유스케이스, 흐름 제어
- Infrastructure: DB, Messaging, 외부 API 등
- Interface: UI, Controller
도메인 개념이 코드에 녹아들어 협업, 유지보수에 좋다.
여기서 특이하게 유스케이스, 흐름 제어가 보이는데,
Port-Adapter 아키텍처
어플리케이션을 비즈니스 로직과 외부 의존성으로 분리하는 아키텍처이다. 이번에 리팩터링 해야 할 아키텍처 대상이다.
- Port: 도메인이 외부와 통신하는 인터페이스(추상화)
- Adapter: 외부 구현체(DB, API, UI)
장점으로는 DB, UI, 메시징 같은 외부 기술을 교체하기가 쉽다.
그리고 테스트하기에도 유리하다(mock adapter).
Port
포트는 꽂는 그 장소를 말한다. 우리의 프로그래밍에서는 특정 함수를 예로 들 수 있는데 가령 login()
이라는 함수가 있다면 우리의 요청이 여기에 들어가 꽂히게 되는 것이다. 이런 것을 Port
라고 한다.
그렇다면 이런 함수를 구현하는 아이는 Interface
이기에 Port 가 자바의 interface 임을 알 수 있다.
Adapter
위에서 login()
이 포트였다면 여기에 들어갈 어댑터가 필요하다. 어댑터는 포트로 연결해주는 아이라고 보면 된다. 따라서 이를 연결시켜주는 것은 MVC
패턴에서 Controller
가 했음을 볼 수 있다. 따라서 Controller 가 어댑터 기능을 하게 된다.
MemberController
MemberService
MemberServiceImpl
이 있다고 하자. 여기서 MemberService
는 포트, MemberController
는 어댑터가 되는 것이다.
primary
위 포트, 어댑터 개념에서 외부의 요청을 직접 받아 동작하는 포트와 어댑터를 주포트, 주어댑터라고 하고 통틀어서 주요소(primary) 라고 한다.
주요소 외에도 이러한 포트 어댑터는 요청 앞단만 있는게 아니라 뒷단에도 있다.
예를 들어 MemberService
라는 서비스는 특정 레포지토리에 접근하여 실제 DB 로의 연결을 통해 데이터를 주고 받게 된다.
이를 접근하게 해주는 것이 Service
인터페이스, Repository
인터페이스 이므로 MemberService
기준으로 이 둘은 포트가 된다.
또한 DB에서의 API 가 Port
, 중간에 껴있는 MemberRepository
구현체가 Adapter
가 되게 된다.
Layered 아키텍처
가장 전통적이고 흔한 방식이다.
- Presentation: Controller, View
- Application, Service: 유스케이스
- Domain: 엔티티, 도메인 로직
- Infrastructure: DB, 외부 시스템
단순하고 직관적이며, 계층 간 의존성이 단방향이다.
FSD 아키텍처
프론트엔드에서 주로 사용하는 방식이며, 기능 단위로 모듈을 나누는 구조이다.
- App
- Process: Deprecated
- Pages
- Widgets
- Features
- Entities
- Shared
단방향이며 모놀리식 서비스에서 기능 단위 패키징 방식으로 사용이 가능하다.
접근제한자
static final
을 Service, Repository 등에 사용하는 것을 구현했는데, 이때 굳이 상수로 취급하여서 둔 이유가 있을까? 가 질문이었다.
우선은 구현 의도는 다음과 같았다.
- 싱글톤 의도에 더 적합하고
- 멀티 스레드 환경에서 실수로 참조를 바꿔치기 하는 것을 막는다.
하지만 이렇게 구현하면 다음과 같은 단점이 생긴다.
- 테스트에 불편하다(Mock 이나 Stub 으로 교체가 안된다)
- 재설정이 불가능하다
- DI 의존성 주입 패턴과 충돌하게 된다.
후자가 더 유연하고 맞는거 같아 후자를 택하여 다시 리팩터링했다.
✒️ 용어
모놀리식 서비스
어플리케이션을 하나의 통합된 코드베이스와 단일 배포 단위로 구성하는 아키텍처이다. 기능이 많더라도 모두 한 프로젝트 안에 들어가고, 하나의 실행 파일로 배포되게 된다.
장점
- 단순
- 개발 속도가 빠름
- 디버깅 쉬움
- 배포가 간단
단점
- 서비스가 커질수록 빌드/배포 시간이 느려짐
- 코드베이스가 거대해져 유지보수가 어려워짐(스파게티 코드화)
- 특정 기능에 장애가 나면 전체 서비스가 다운됨
- 기술 스택을 기능 별로 다르게 쓰기 힘듦
마이크로 서비스
기능을 서비스 단위로 쪼개서 독립적으로 배포하고 운영한다.
회원서비스 따로, 주문 서비스 따로 등등