Server

[Server] 객체에게 역할과 책임을 부여하는 객체 지향 프로그래밍(Object-Oriented Programming)

망나니개발자 2024. 11. 5. 10:00
반응형

 

 

1. 객체에게 역할과 책임을 부여하는 객체 지향 프로그래밍
(Object-Oriented Programming)


[ 객체에게 역할과 책임을 부여하는 객체 지향 프로그래밍 ]

우리는 대부분 자바 또는 코틀린과 같은 객체 지향 언어(Object-Oriented Programming)를 사용하여 개발을 하고 있다. 객체 지향 언어는 캡슐화(Encapsulation), 상속 (Inheritance), 다형성 (Polymorphism), 추상화 (Abstraction) 등과 같은 특성을 통해 프로그래밍을 용이하게 할 수 있도록 도와준다. 이러한 특성을 바탕으로 시스템을 구현하면 변경에 유연하게 대응할 수 있다.

객체 지향적인 부분이 가장 많이 적용되는 부분은 인터페이스를 통해 추상화를 하는 부분일 것이다. 예를 들어 슬랙으로 메시지를 보내는 클래스가 있다고 하자.

@Component
class SlackAdapter(
    private val slackProperties: SlackProperties,
    private val webClient: WebClient,
) : SendSlackPort {

    override fun send(message: String) {
        webClient.call<String>(
            host = slackProperties.host,
            method = HttpMethod.POST,
            uri = slackProperties.alertUrl,
            requestBody = SendSlackAlertPayload(message),
        )
    }
}

 

 

테스트가 실행될 때마다 슬랙 메시지가 발송되면 상당히 노이지하고 우리의 집중력을 흩트려 놓기 쉽다. 따라서 다음과 같이 인터페이스를 통해 추상화하고 다형성을 활용하여 테스트 환경에서는 메시지가 발송되지 않도록 개선할 수 있다.

@Profile("!test")
@Component
class SlackAdapter(
    private val slackProperties: SlackProperties,
    private val webClient: WebClient,
) : SendSlackPort {

    override fun send(message: String) {
        webClient.call<String>(
            host = slackProperties.host,
            method = HttpMethod.POST,
            uri = slackProperties.alertUrl,
            requestBody = SendSlackAlertPayload(message),
        )
    }
}

@Profile("test")
@Component
class TestSlackAdapter : SendSlackPort {

    override fun send(message: String) {
		    // Do nothing.
    }
}

 

 

이러한 추상화를 통해 우리는 변경을 매우 용이하게 할 수 있으며, SOLID 원칙 중에서 개방 폐쇄 원칙 (Open-Closed Principle, OCP)과 의존 역전 원칙 (Dependency Inversion Principle, DIP)을 준수할 수 있다.

 

 

 

 

[ 객체에게 역할과 책임을 부여하는 객체 지향 프로그래밍 ]

이렇듯 추상화와 다형성을 적용하는 것은 대표적인 객체 지향 프로그래밍 기법 중 하나이다. 하지만 캡슐화를 통해 객체 내부의 상태를 숨기고, 객체에게 메시지를 전송하여 객체가 자율적이고 능동적인 존재가 되도록 하는 것 역시 객체 지향 프로그래밍이다.

예를 들어 18세 이하의 사용자만 프로모션에 참여 가능하다는 정책이 있다고 하자. 이를 다음과 같이 구현할 수 있다.

@Service
class MemberService(
    private val memberRepository: MemberRepository,
) {

    fun canParticipate(memberId: Long) {
        val member = memberRepository.findById(memberId)
            ?: throw NoSuchElementException()

        return canParticipate(member)
    }
    
    fun canParticipate(member: Member) {
        return member.age <= MAX_PROMOTION_AGE
    }

    companion object {
        const val MAX_PROMOTION_AGE = 18
    }
}

 

 

하지만 위와 같은 구현 방식은 여러 가지 문제가 존재한다. 먼저 여러 가지 비즈니스 정책들이 MemberService에 존재하여, Member와 관련된 처리를 위해서는 모든 곳에서 MemberService를 주입받아야 한다. MemberService에 대한 의존관계 주입이 불필요한 상황임에도 불구하고 올바르지 못한 구현 방식 때문에 시스템이 복잡해질 수 있다. 또한 MemberService는 Member와 관련된 모든 비즈니스 로직이 존재할 것이므로, 실제 서비스를 운영하는 관점에서는 상당히 많은 비즈니스 로직을 갖게 될 것이다. 이러한 서비스를 FatService라고 부르며, 심지어 FatService에 대한 테스트는 성공과 실패 케이스를 모두 고려하면 훨씬 복잡하여 관리가 어려워진다.

그 외에도 여러 가지 문제들이 존재하지만, 가장 큰 문제는 객체가 단순히 데이터 저장만을 담당하는 존재로 전락한다는 것이다. 위와 같은 구현에서 Member는 다음과 같이 데이터 만을 주고 받는 단순 자료 구조일 뿐이며, 어떠한 역할과 책임도 부여받지 못한 수동적인 존재이다.

data class Member(
    val name: String,
    val age: Int,
)

 

 

하지만 다음과 같이 Member 객체에게 역할과 책임을 부여하고, 자율적이고 능동적인 존재로 만들 수 있다. 이렇게 하면 위에서 언급한 여러 가지 문제들을 해결할 수 있을 뿐만 아니라, 응집도 높은 객체를 갖게 되어 변경에 용이하게 대처할 수 있다.

data class Member(
    val name: String,
    val age: Int,
) {

    fun canParticipatePromotion(): Boolean {
        return age <= MAX_PROMOTION_AGE
    }

    companion object {
        const val MAX_PROMOTION_AGE = 18
    }
}

 

 

객체에게 올바른 역할과 책임을 부여하고, 협력할 수 있도록 하려면 가장 먼저 객체의 상태는 숨기고 행동만을 외부에 공개해야 한다. 이처럼 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화라고 부른다. 캡슐화의 목적은 변경하기 쉬운 객체를 만드는 것이다. 캡슐화를 통해 객체 내부로의 접근을 제한하면 객체와 객체 사이의 결합도를 낮출 수 있기 때문에 설계를 좀 더 쉽게 변경할 수 있게 된다.

객체 내부에 대한 접근을 통제하고 객체 스스로 판단하고 행동하도록 하면 객체를 자율적이고 능동적인 존재로 만들 수 있다. 객체에게 원하는 것을 요청하고 객체 스스로 최선의 방법을 결정하도록 하는 것이다. 이러한 방식으로 설계 및 개발을 하게 되면 자연스레 응집도는 높고 결합도는 낮은 시스템을 갖출 수 있을 것이다.

 

 

 

객체에게 올바른 역할과 책임을 부여하고, 협력 가능하도록 만드는 것은 조영호님의 오브젝트 책에 자세히 나와 있다. 책의 내용이 다소 두꺼운데, 요약본을 정리해두었으니 관련 내용을 읽어보도록 하자.

 

 

 

 

 

반응형