
📚 개요
이직 준비가 길어져서 스터디를 구했는데 스터디 그룹 내에서 프로젝트를 해보자! 라는 얘기가 나왔습니다.
MSA 환경에서의 개발 및 운영을 해보기 위해서 프로젝트를 진행하기로 했습니다.
MSA 는 많은 서비스와의 연결이 있기에 MVC패턴보단 헥사고날 아키텍처가 적합하다 판단했습니다.
이를 적용하는 과정에서의 학습지식, 문제점 등을 정리 해보려합니다.
📚 준비물
1. 정말 단단한 멘탈
2. 아키텍처에 대한 관심과 열정
3. Kotlin + Spring Boot
4. 틀린부분 있으면 태클 걸어주는 친절함 😉
😢 헥사고날 아키텍처 (클린아키텍처, 포트어댑터패턴) 의 탄생 배경
기존의 레이어드 아키텍처 (e.g. MVC)는 의존성의 관계가 Controller -> Service -> Repository -> 외부세계 로 이어집니다.
그로인해 외부세계의 변경이 있다면 Repository도 변경이 생기고, 그렇다면 Service도 변경, Controller도 변경 될수가 있다는 문제가 있습니다.
즉, 추상화의 정도가 낮은 계층 -> 추상화의 정도가 높은 계층 라는 문제가 생기죠.
예시로 보겠습니다.
유저의 정보를 불러오는 메소드가 존재하는데, 기존에는 개인정보 암호화가 되어있지 않아서 Repository에서 그냥 꺼내오면 됐습니다.
@Service
class service(
private val repository:Repository
) {
fun getUserInfo(userId: Long) {
val user = repository.findById(userId).orElseThrow { NotFoundException("못찾음") }
// do something
}
}
@Repository
interface Repository: JpaRepository<User,Long> {}
하지만 개인정보 암호화가 적용(레포지토리 레이어의 변경사항)이 됐다고 한다면 어떻게 될까요?
@Service
class service(
private val repository:Repository
) {
fun getUserInfo(userId: Long) {
val user = repository.findById(userId).orElseThrow { NotFoundException("못찾음") }
val decryptUserInfo = decrypt(user) // 복호화가 추가됨
// do something
}
}
@Repository
interface Repository: JpaRepository<User,Long> {}
위 로직처럼 복호화 로직이 서비스단에 추가가 될 것입니다.
이 말은 즉, 레포지토리 레이어의 변경이 서비스의 변경으로 이어지는 문제가 발생 한다는 것입니다.
이 문제를 해결하기 위해서는 가장 간단한 방법은 다음과 같이 추상화를 하고 추상화를 의존하면 되긴합니다.
@Service
class service(
private val repository:Repository
) {
fun getUserInfo(userId: Long) {
val user = repository.findById(userId)
// do something
}
}
@Repository
interface Repository {
fun findById(userId:Long):Optional<User>
}
class RepositoryImpl(
private val jpaRepository: JpaRepository
) {
override fun findById(userId:Long):Optional<User> {
val user = repository.findById(userId).orElseThrow { NotFoundException("못찾음") }
return decrypt(user) // 복호화 할지 말지를 구현체에서 결정
}
}
interface JpaRepository: JpaRepository<User,Long> {}
위 코드와 같이 진행한다고하면, Repository의 변경이 있다고 하더라도
하지만 위와 같이 추상화를 진행하게 되면 레이어드 아키텍처의 철학을 위배하게 됩니다.
1. 각 계층은 위계를 가진다.
2. 각 계층은 계층에 맞는 역할을 가진다.
3. 각 계층은 하위 계층을 의존한다.
이 중 3번 각 계층은 하위계층을 의존한다. 라는 철학을 위배하게 됩니다.
철학이라 하니 좀 이해가 안가는데 그림으로 보면 이해가 쉽습니다.
Service에서 Repository 인터페이스를 의존하는데, 이 Repository 인터페이스는 역할로 봤을 때 Service Layer 에 속합니다.
Repository 인터페이스가 Service 계층이 Persistence 계층으로부터 요구하는 요구사항을 정의하고 알고있기에 Service에 속합니다.
(마찬가지로 한국에 존재하는 service, serviceImpl 패턴 또한 이 철학을 위배한다 생각합니다. 제 주관이긴 합니다 🙂)
그래서 새로운 철학을 가진 헥사고날 아키텍처가 탄생하게 됩니다. (by. Alistair Cockburn)
🎓 헥사고날 아키텍처
🧐 철학
외부의 변화로부터 애플리케이션 내부의 변경을 최소화 한다.
라는 철학을 가졌습니다.
헥사고날 아키텍처는 기본적으로 아래의 구조를 가졌습니다.
기본적 뼈대만 그리면 위와 같은 구조를 가졌습니다
Adapter -> InputPort -> Usecase -> Entity <- Usecase -> OutputPort -> Adapter
구조만 봐도 알겠지만, Entity가 중심이 되는 구조입니다.
언뜻보면 정말 복잡해 보입니다만, 나눠서 보게 된다면 생각보다는 그렇게 복잡하지 않다 라는 것을 느낄 수 있습니다.
우선, 구조상 가장 큰 특징을 보겠습니다.
우선 각 계층을 나눕니다.
각 레이어를 칭하는 이름은 너무 많고, 아키텍처에는 정답이 없어서 각자 프로젝트에 맞춰서 적용하여 사용합니다.
저는 위 그림을 기반으로 설명하겠습니다.
- Infrastructure Hexagon ( Presentation, Adapter, Framework 등등 )
- Adapter를 통해서 외부 세계와 연결되는 영역입니다. MVC 패턴에서는 Controller와 Repository 레이어가 속할 수 있습니다
- Application Hexagon ( Usecase, Service 등등 )
- Port를 통해서 Infrastructure 과 연결되는 영역입니다. MVC 패턴에서는 Service 에 해당합니다.
- Domain Hexagon ( Core, Entity 등등 )
- 비즈니스 로직을 수행하는 주체입니다. MVC 패턴에서는 별도로 존재하지 않지만, DDD 에서 말하는 Domain과 같은 개념입니다
중간중간에 Adapter , Port 를 통해서 각 계층을 이동하는 걸 볼 수 있습니다.
이 Adapter , Port 는 모두 내부의 영역을 추상화한 인터페이스입니다
레이어드 아키텍처와 다르게 헥사고날 아키텍처는 높은 추상화가 낮은 추상화를 의존합니다.
그 말은 즉 더 높은 수준의 구현체로 낮은 수준의 구현체가 의존하는 방향으로 구조가 진행됨을 말합니다.
🤔 그래서 장점이?
헥사고날 아키텍처는 다음과 같은 장점을 가집니다.
1. 외부 환경의 변화로부터 Application 의 변경이 최소화됩니다.
2. 추상화된 인터페이스를 DI 받는 클래스를 테스트하기 쉬워집니다.
3. 비즈니스 모델이 도메인에 집중되어 외부 환경을 신경쓰지 않고 개발해도 됩니다.
4. 관심사 분리로 인해서 문제되는 부분을 찾아내기 쉽습니다. ( 브레이크 포인트 등을 활용한 디버깅은 어려워지지만, 에러 핸들링의 정도에 따라서 에러를 더 빨리 찾아낼 수도 있습니다. )
장점들 중 한가지만 꼽아서 코드로 보겠습니다.
외부 환경의 변화로부터 Application의 변경이 최소화 되게되는 형태입니다
@RestController
class Controller(
private val usecase:Usecase
) {
@GetMapping("/user")
fun getUser(@RequestParam userId: Long) {
usecase.getUserInfo(userId)
}
}
@Service
interface Usecase {
fun getUserInfo(userId: Long): User
}
class InputPort(
private val outputPort: OutputPort
):Usecase {
fun getUserInfo(userId: Long):User {
val user = outputPort.findById(userId)
// do something
return user
}
}
@Repository
interface OutputPort {
fun findById(userId:Long):User
}
class Adapter(
private val jpaRepository: JpaRepository
) {
override fun findById(userId:Long):Optional<User> {
return repository.findById(userId).orElseThrow { NotFoundException("못찾음") }
}
}
interface JpaRepository: JpaRepository<User,Long> {}
위 소스는 구현체 (클래스) 에서 변경점이 생긴다 하더라도 인터페이스를 주입받는 클래스에서는 구현체의 내용을 알 수가 없기에 클래스 내부의 소스를 변경할 필요가 없어집니다.
(위 소스는 아래의 그림을 코드로 옮긴 형태입니다)
🫢 usecase 가 인터페이스고 InputPort가 클래스네?
이 부분에 대해서 고민이 많았고 앞서 말씀드린 것처럼 아키텍처는 구현하기 나름입니다.
제가 생각한 방식은 usecase 라는 것은 actor의 행동을 정의한 것
이 말은 사실 인터페이스의 정의와 거의 유사하다고 판단했고 usecase는 인터페이스에 좀 더 가깝겠다는 판단이 섰습니다.
따라서 usecase 를 interface로 지정했고, 이를 구현하는 것을 inputPort로 명명했습니다.
(명명에 대한 부분은 개발팀의 컨벤션에 맞추면 될 듯 합니다.)
헥사고날 아키텍처를 적용하는데 있어서는 배경, 철학에 대해서 생각해보고 강조하는 부분을 유의해서 개발하게 되기만 하면 됩니다.
디렉토리 또한 개발팀 내부 인원들과의 협의를 바탕으로 진행하면 됩니다.
there is no silver bullet
아키텍처를 설계함에 있어서 가장 잘 어울리는 말인것 같습니다.
아키텍처에 정답은 없습니다.
👇 참고로 이건 제 디렉토리입니다 ㅎ..
https://behoney.tistory.com/84
좋은 패키지 구조를 찾아서 (with. Port And Adapter)
📚 개요오랜만에 제가 만들던 프로젝트의 패키지 구조를 살펴봤는데 난해하고 이걸 어디에 둬야지 적절하지(?)라는 의문이 들어서 디렉토리 구조를 리팩토링 해보자 라는 목표로 패키지를 재
behoney.tistory.com
디렉토리는 구성하기 나름이긴 합니다.
⁉️ 기업들은 헥사고날 아키텍처를 어떻게 사용할까?
찾으면 찾을 수록 매 조직마다 다 다르게 쓰니 개념이 꼬이고 헷갈리기도 했습니다.
저 처럼 삽질 해가면서 힘들게 공부하지 마시라고 준비했습니다
이 사진을 기준으로 정리해봤습니다
각 명칭에 맞춰서 조직마다 쓰는 용어를 정리했고, Service 면 UserService 이런 형태라고 봐주시면 됩니다
Domain은 전부 각 엔티티, 도메인에 맞는 이름을 사용했기에 따로 정의 할 수 없을 듯 해서 뺏습니다.
제가 참고한 링크에 있는 정보에 없다면 비공개라고 작성했습니다.
회사 | Controller (C) | Usecase (I) | Inputport (C) | OutputPort (I) | Adapter (C) | 관련 Url |
라인 | Controller | Service | ServiceImpl | Repository | 비공개 | 기술블로그 |
네이버페이 | Controller | Usecase | Service | OutPort | Adapter | 기술블로그 |
카카오페이 | Controller | ClientPort | Service | ServicePort | ServiceAdapter | 기술블로그 |
NHN | Controller | Usecase | Service | Port | Adapter | 유튜브 |
퓨처위즈 | Controller | Usecase | Service | Port | Adapter | 기술블로그 |
배민 | Controller | Usecase | InputPort | OutputPort | Adapter | 기술블로그 |
SK | WebAdapter | Port | Service | Port | Adapter | 기술블로그 |
당근 | 비공개 | 비공개 | Service | Port | Adapter | 기술블로그 |
⭐️ 나 ⭐️ | Controller | Usecase | InputPort | OutputPort | Adapter | Here 🙂 |
긴 글 읽어주셔서 감사합니다.
재취업의 길은 멀고도 험하다..
Keep XP 🔥🔥🔥
'CS > ARCHITECTURE' 카테고리의 다른 글
좋은 패키지 구조를 찾아서 (with. Port And Adapter) (0) | 2025.04.03 |
---|
백엔드는 못말려
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!