스프링의 핵심 인터페이스 BeanFactory
와 그것을 상속하는 ApplicationContext
는 각종 Managed Bean의 정보를 담고 있습니다.
의존 관계의 경우 대부분이 초기화 과정에서 @Autowired
애노테이션을 사용한 필드 주입, setter 주입, 생성자 주입등의 경로를 통해 의존 관계가 해결됩니다.
의존 관계 주입은 위 방법중에서도 생성자 주입이 권장되며, 가장 좋은 방법이지만 코드를 작성할 때는 위 세 가지 방법으로 의존 관계를 해결할 수 없는 상황이 분명히 존재합니다.
예를 들어 요청 DTO를 저장하기 위한 엔티티로 변경하는 경우 DTO 내부에 toEntity()
라는 메서드를 정의하여 서비스 레이어에서 사용하면 코드가 깔끔히 분리되며 책임이 명확해집니다.
// 사용자 DTO
class UserForm private constructor() {
// 회원가입 시 사용할 요청 DTO
class SaveByUser {
var name: String? = null
var username: String? = null
var password: String? = null
// DTO -> 엔티티
fun toEntity(): User =
User()
.also { user ->
user.name = this.name
user.username = this.username
user.password = this.password
}
}
}
이렇게 하는 경우 서비스 레이어는 온전히 로직에만 집중할 수 있는 장점이 있습니다. 하지만 비밀번호의 경우, 저장하기 전 단방향 암호화라는 과정을 거쳐야 하고 스프링 시큐리티를 사용하고 있는 프로젝트의 경우 그 역할은 PasswordEncoder
인터페이스를 구현한 스프링 빈이 가지고 있습니다.
당연하게도 DTO는 스프링의 Managed Bean이 아니기 때문에 스프링에게 빈 주입을 요청할 수 없습니다.
이러한 상황에서 서비스 레이어에서 처리가 필요한 항목만 다시 처리하여 진행해도 되지만 그렇게 되면 DTO -> 엔티티 변환 책임이 분산되어 버리는 단점이 있습니다.
@Validated
@Service
class UserServiceImpl(
private val passwordEncoder: PasswordEncoder,
) : UserService {
override fun save(
@Valid
form: UserForm.SaveByUser,
) : UserDto {
val entity = form.toEntity()
// DTO -> 엔티티 변환은 아래 로직이 있어야만 완전해집니다..
entity.password = this.passwordEncoder.encode(entity.password)
// save
}
}
따라서 개인적인 생각으로는 제반 상황에서만 스프링 빈에 정적인 방법으로 접근할 수 있는 API를 제공해 주게 된다면 DTO -> 엔티티 변환 로직에 있어 확실한 책임 분리 효과를 얻을 수 있다고 생각합니다.
@Configuration(proxyBeanMethods = false)
class BeanRegistry : ApplicationContextAware {
companion object {
private lateinit var applicationContext: ApplicationContext
// 타입으로 빈을 가져옵니다
fun <T : Any> getBean(type: KClass<T>): T =
applicationContext.getBean(type.java)
// 이름과 타입으로 빈을 가져옵니다
fun <T : Any> getBean(name: String, type: KClass<T>): T =
applicationContext.getBean(name, type.java)
}
override fun setApplicationContext(applicationContext: ApplicationContext) {
BeanRegistry.applicationContext = applicationContext
}
}
BeanRegistry
라는 클래스를 정의하고 ApplicationContextAware
인터페이스를 구현하게 되면 스프링 초기화 완료 이후 시점부터는 어디서든 접근이 가능하게 됩니다.
물론 어떤 빈이 있는지 정확히 인지하고 사용해야 하기 때문에 안티패턴이라고 생각하지만 제한적으로 사용하기에는 유용합니다.
BeanRregistry
를 사용하면 위 코드는 다음과 변경될 수 있습니다.
// 사용자 DTO
class UserForm private constructor() {
// 회원가입 시 사용할 요청 DTO
class SaveByUser {
var name: String? = null
var username: String? = null
var password: String? = null
// DTO -> 엔티티
fun toEntity(): User =
User()
.also { user ->
user.name = this.name
user.username = this.username
user.password = encode(this.password!!)
}
// 비밀번호를 해싱합니다.
private fun encode(rawPassword: String): String =
// 스프링이 가지고 있는 PasswordEncoder를 사용합니다.
BeanRegistry.getBean(PasswordEncoder::class)
.encode(rawPassword)
}
}
위 방법은 일종의 꼼수로, 가장 좋은 방법은 DTO -> Entity 를 변환하는 레이어를 컨트롤러와 서비스 사이에 끼워 넣는 게 가장 좋은 방법이라고 생각합니다.