
비즈니스 로직 영역이며 외부 영역과 직접 통신하지 않고 port 를 통해 통신
외부에서 들어오는 요청 처리 영역
request 요청 -> inbound adapter -> inbound port -> 비즈니스 로직(service) -> outbound port -> outbound adapter -> response 반환

패키지 구조는 위와 같이 크게 4가지로 나누었습니다.
간단하게 회원가입 api를 작성해봅시다.
package com.demo.user.adapter.out.persistence.entity
@Entity
@Table(name = "USER")
class UserJpaEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
@Column(name = "EMAIL")
var email: String,
@Column(name = "NICK_NAME")
var nickName: String,
@Column(name = "PASSWORD")
var password: String,
) {
}
package com.demo.user.domain
data class User(
val id: Long? = null,
val email: String,
val nickName: String,
val password: String
)
package com.demo.user.adapter.in.rest
@RestController
@RequestMapping(value = ["/user"])
class UserController(
// 내부 영역 사용을 위한 usecase 주입
private val userUseCase: UserUseCase
) {
@PostMapping
fun create(@RequestBody dto: UserDto.CreateUser): ResponseEntity<GenericResponse<UserDto.Response>> {
val command = UserCommandMapper.instance.toCreateUserCommand(dto)
val user = userUseCase.create(command)
return GenericResponse.ok(UserResponseDtoMapper.instance.toDto(user))
}
}
package com.demo.user.adapter.in.rest.dto
sealed class UserDto {
data class CreateUser(
val nickName: String,
val password: String,
val email: String
) : UserDto()
data class Response(
val id: Long,
val nickName: String,
val email: String
) : UserDto()
}
package com.demo.user.port.in.command
sealed class UserCommend {
data class CreateUser(
val nickName: String,
val password: String,
val email: String
) : UserCommend()
}
package com.demo.user.port.in.mapper
@Mapper
abstract class UserCommandMapper {
companion object {
val instance: UserCommandMapper = Mappers.getMapper(UserCommandMapper::class.java)
}
abstract fun toCreateUserCommand(dto: UserDto.CreateUser): UserCommend.CreateUser
}
package com.demo.user.adapter.in.rest.mapper
@Mapper
abstract class UserResponseDtoMapper {
companion object {
val instance: UserResponseDtoMapper = Mappers.getMapper(UserResponseDtoMapper::class.java)
}
abstract fun toDto(user: User): UserDto.Response
}
dto를 그대로 넘기지 않고 command를 사용하는 이유는 외부 시스템 변경 시 usecase도 같이 변경 되기 때문에 usecase 요청 값을 따로 command class 로 분리
package com.demo.user.port.in.usecase
interface UserUseCase {
fun create(commend: UserCommend.CreateUser): User
fun findById(id: Long): User?
fun findByEmail(email: String): User?
}
package com.demo.user.application.service
// usecase 를 구현하는 구현체, 비즈니스 로직을 담당
@Service
class UserService(
private val userJpaPort: UserJpaPort
) : UserUseCase {
@Transactional
override fun create(commend: UserCommend.CreateUser): User {
if (findByEmail(commend.email) != null) {
throw CommonException(CommonExceptionCode.USER_ALREADY_EXISTS)
}
return userJpaPort.saveUser(commend)
}
override fun findByEmail(email: String): User? {
return userJpaPort.findByEmail(email)
}
}
package com.demo.user.adapter.out.persistence.repository
interface UserRepository : JpaRepository<UserJpaEntity, Long> {
fun findByEmail(email: String): Optional<UserJpaEntity>
}
package com.demo.user.port.out
// 내부 영역이 외부 영역을 사용하기 위한 통로
interface UserJpaPort {
fun saveUser(commend: UserCommend.CreateUser): User
fun findByEmail(email: String): User?
}
package com.demo.user.adapter.out.persistence.repository
// outbound port 구현체
@Component
class UserPersistAdapter(
private val userRepository: UserRepository
) : UserJpaPort {
override fun saveUser(commend: UserCommend.CreateUser): User {
val userJpaEntity = userRepository.save(UserJpaEntityMapper.instance.toJpaEntity(commend))
return UserJpaEntityMapper.instance.toUser(userJpaEntity)
}
override fun findByEmail(email: String): User? {
val userOptional = userRepository.findByEmail(email)
return if (userOptional.isPresent) UserJpaEntityMapper.instance.toUser(userOptional.get()) else null
}
}
package com.demo.user.adapter.out.persistence.mapper
@Mapper
abstract class UserJpaEntityMapper {
companion object {
val instance: UserJpaEntityMapper = Mappers.getMapper(UserJpaEntityMapper::class.java)
}
abstract fun toJpaEntity(comment: UserCommend.CreateUser): UserJpaEntity
@Mapping(target = "roles", ignore = true)
abstract fun toUser(userJpaEntity: UserJpaEntity): User
}
기존 3티어로 된 계층형 아키텍쳐 에서는 비즈니스 로직과 외부 요소가 강하게 결합 되어 있어서 유연성이 없어지고 변경에 취약합니다. 위와 같이 헥사고날 아키텍쳐로 구현했을 때 내부, 외부 영역을 나누고 adapter, port 를 통해서만 통신하도록 구현했을 때 내부 영역은 외부 영역에 전혀 의존 하지 않게 됩니다. usecase 별로 나누어 종속성을 없애고 단일 책임을 갖도록 설계할 수 있습니다.
*공부하면서 주관적인 견해로 작성된 글이라 틀린 내용이 있을 수 있습니다... 피드백은 언제나 환영입니다!