객체지향 학습 (1)

Java로 코드를 작성하는데 객체지향 패러다임뿐만 아니라 상속, 캡슐화 같은 개념들조차도 진짜 개념적으로만 사용하고 "왜 사용해야 하는가?"라는 질문에 답을 할 수 없었다. 매번 생성형 AI를 이용해서 객체지향을 학습하려고 도전해 봤지만, 객체지향 패러다임에 대해 아무것도 모르는 내 입장에서 AI가 제공해 주는 정보가 진실인지 거짓인지 판별할 지식조차 존재하지 않아 이게 학습하는 것이 아니라 시간을 버리는 느낌을 받았다.

 

그래서, 가장 쉽고 빠르게 접근할 수 있는 방법인 책을 이용하기로 마음먹었다. 온라인 서점에 객체지향을 검색해 보면 리뷰가 많은데 평점도 좋은 책이 보이는데 바로  객체지향의 사실과 오해이다.

 

이 책을 3주 정도 시간을 들여 완독 하며 객체지향 패러다임을 개념적으로 알 수 있었고 이를 코드로 옮겨가는 작업에 대해서 궁금해 바로 『오브젝트』라는 책을 구매했다. 해당 책을 읽으며 객체지향 패러다임에 대해 기록하고 싶은 내용을 적어보려고 한다.

 

 

규칙을 적어놓고 지키려고 노력하기

 

『오브젝트』  객체지향의 사실과 오해  에서 나왔던 개념들을 한번 정리해 보았다.

- 메시지를 중심으로 객체를 설계하기
- 세부적인 지시는 지양하고 어떤 작업에 대해 요청하기
- 하나의 메서드는 하나의 작업만 수행한다.
- 항상 예외 케이스를 최소화하고 일관성을 유지할 수 있는 방법 선택하기
- 처음부터 완벽한 설계는 불가능하다. 설계를 코드로 옮기는 과정에서 설계를 수정해도 좋다.

 

이 중, 『오브젝트』 책에 나온 예제를 먼저 코드로 옮겨보며 내가 갖고 있는 문제점을 몇 가지 발견할 수 있었다. 요청을 너무 세부적으로 분리하고 있었고, 항상 구현하다 나오는 예외케이스는 조건문으로 처리하고 있었다는 것을 확인할 수 있었다. 이런 부분은 사실 직접적으로 누군가에게 코드리뷰를 받지 않는다면 발견하기 힘든 부분들이라고 생각한다. 항상 습관적으로 작성하던 행동을 어떻게 잘못된 습관이라는 것을 발견하고 고칠 수 있을까? 

 

아래 링크는 예제를 먼저 코딩하고 책에 나온 내용으로 리팩토링을 진행해 보며 내가 갖고 있는 문제점을 간접적으로 코드리뷰를 Commit으로 남겨놓은 레포지토리이다. 만약 지금 당장 객체지향 패러다임을 공부하고 싶다면, 오브젝트를 읽을 때, 예제가 나오면 직접 코딩을 먼저 해보고 예제를 따라가는 방식으로 한번 공부해 보는 걸 강력 추천한다.

커밋 기록

 

 

 

java-play-ground/practice-oop-theater at main · HeeChanN/java-play-ground

java로 이것저것. Contribute to HeeChanN/java-play-ground development by creating an account on GitHub.

github.com

 

앞으로 계속 책을 읽어나가며 객체지향 패러다임에 필요한 개념들을 더 정리해 나가며 앞으로 특정 요구사항을 구현해 나갈 때 저 원칙들을 잘 지키려고 노력하며 개발해야겠다.

 

 

 

객체지향 패러다임 프로세스

책을 읽으며 가장 좋았다고 생각한 것은 항상 "객체지향으로 코드를 작성하려면 어떻게 하면 시작하면 될까?"라는 고민을 갖고 있었는데 이 고민을 해결해 줬다는 점이다. 이 부분은 객체지향의 사실과 오해라는 책에서 나오는 내용으로 모든 경우에 다 맞진 않을 수 있다. 그래도 처음 공부할 때는 이런 가이드라인이 존재한다면 그 가이드라인대로 체화할 수 있다고 생각한다.

 

바로, 어떻게 객체지향 패러다임을 지키며 코드를 작성할지에 대한 순서를 명확하게 제시해 줬다. 그 내용을 정리해 보면 다음과 같다.

1. 요구사항을 분석한다.
    - 요구사항 속에서 주요 객체들을 뽑고 객체사이의 협력관계를 파악한다. 
    - 객체의 윤곽이 잡히면 공통된 특성과 상태를 가진 객체들을 타입으로 분류한다.
2. 도메인 모델 만들기
    - 객체들 그리고 협력관계를 바탕으로 메시지를 주고받는 상호작용을 표현하는 다이어 그램을 만들어 본다.
3. 메시지를 기반으로 클래스를 설계하기
    - 요구사항 속에서 발견한 하나의 기능을 잘게 분해하면 여러 객체의 요청과 응답으로 구성되어 있음을 파악할 수 있다. 이를 바탕으로 각 객체에게 역할을 부여하여 책임을 할당한다.
4. 각 메시지의 실제 동작과정을 구현한다.
    - 요청에 대해 객체가 처리해야 하는 일련의 작업들이 많다면 메서드를 분리해서 하나의 메서드에는 하나의 기능만 담는다.
    - 이 과정 속에서, 설계와 구현의 차이점을 발견하고 도메인 모델을 수정해 나간다.

 

이 내용을 실제로 적용해 보려고 했지만 생각보다 쉽지 않았다. 돌이켜 보면 ‘내가 제대로 하고 있는 걸까?’라는 의심이 계속 떠올랐던 게 가장 큰 이유였던 것 같다. 그래도 판단과 결과물에 대한 피드백을 받으려면 끝까지 코드를 만들어 봐야 무엇이 잘못됐는지 드러난다고 생각했다. 그래서 『오브젝트』에 나오는 영화 예매 시스템을 대상으로 객체를 식별하고 도메인 모델을 설계한 뒤, 이를 코드로 구현해 보았다. 아래 레포지토리의 README를 보면 도메인 도메인 모델까지의 과정을 볼 수 있고, 커밋을 따라가 보면 구현까지 볼 수 있다.

 

java-play-ground/practice-oop-movie-reservation at main · HeeChanN/java-play-ground

java로 이것저것. Contribute to HeeChanN/java-play-ground development by creating an account on GitHub.

github.com

 

이 과정을 통해 여러 문제점을 발견했다. 가장 먼저, 요구사항을 제멋대로 바꿔 구현했다는 점이다. 도메인 모델에서는 분명히 영화 → 할인정책 → 할인조건 순으로 메시지가 흘러가도록 설계했는데, 코드를 쓰다 보니 설계를 보지 않은 채 영화가 할인정책과 할인조건에 직접 요청을 보내도록 바꿔 버렸다. 작성하는 순간에는 이를 인지하지 못했고, 결국 코드를 다 짠 뒤 설계 모델과 반드시 비교해야 한다는 교훈을 얻었다.

 

또 하나는 Spring Boot로 개발하던 습관 때문이다. 평소 DTO가 엔티티를 의존하지 않게 하려고 컨트롤러에서 넘어온 DTO에서 필드를 하나씩 꺼내 전달하던 버릇이 여기서도 작동했다. 책에서는 상영(Screening) 객체를 메시지로 건네며 협력을 구성했지만, 나는 필드 단위로 값만 전달하는 방식으로 풀었다. “의존성을 줄이자”는 의도였지만, 결과적으로 객체 간 협력보다 세부 구현에 초점이 맞춰졌고, 메서드 내부가 불필요하게 복잡해졌다. (아래 코드를 보면 예외케이스도 조건문으로 처리하는 것을 볼 수 있다.)

 

이번 경험을 통해 가장 단순한 협력부터 먼저 구현하고, 그다음 리팩터링으로 트레이드오프를 드러내며 조정해 나가는 편이 훨씬 낫다는 걸 깨달았다.

 

이 과정을 통해 되게 많은 내 코딩 습관들을 발견하고 있는데, 아마 그냥 책만 읽어 나갔다면 아주 오랫동안 발견하지 못했을 것 같다.

 

 

다형성, 상속, 합성

객체지향 패러다임을 지키며 코드를 구현할 때 사용할 수 있는 프로그래밍 방법에는 다형성, 상속, 합성이 존재한다. 합성을 빼고는 모두 언어를 배울 때 한 번씩 들어봤을 개념인데, 이 책은 이 내용에 대해 좀 더 설계적인 측면에서 바라본 관점이 정말 좋아서 정리해 보려고 한다.

 

`상속`

상속은 클래스 사이의 관계를 설정하여 기존 클래스가 가지고 있는 모든 속성과 행동을 새로운 클래스에 포함시킬 수 있다. 상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스(메시지)를 자식 클래스가 물려받을 수 있기 때문이다. 자식 클래스는 상속을 통해 부모 클래스의 인터페이스를 물려받기 때문에 부모 클래스 대신 사용될 수 있다. (업 캐스팅)

 

`다형성`

다형성은 컴파일 시간의 의존성과 런타임 실행시간의 의존성이 다를 수 있다는 사실을 기반으로 한다. 동일한 메시지를 수신했을 때, 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다.(동적 바인딩)

public class Movie {

    private DiscountPolicy discountPolicy;
		
    ...
    
    public Movie(DiscountPolicy discountPolicy, ...){
    	this.discountPolicy = discountPolicy
        ...
    }
}

Movie는 컴파일 타임에 할인 정책의 인터페이스(예: DiscountPolicy)에 의존합니다. 런타임에는 생성자를 통해 주입된 구현체동적 바인딩되어 동작하며, 이것이 곧 다형성입니다. 이 구조 덕분에 새로운 정책을 추가해도 Movie를 수정하지 않고 확장할 수 있고, 기존 코드는 변경을 최소화할 수 있습니다. 『오브젝트』 에서는 예외 케이스를 조건문이 아닌 다형성으로 해소하는 리팩터링 과정을 보여 주며 설명하는데 해당 부분을 직접 읽어 보시는 것을 추천합니다.

 

그럼 합성은 무엇일까? 

 

합성(composition)은 한 객체가 다른 객체를 필드로 보유하고 그 객체에 일을 위임하는 설계다. Spring Boot에서 컨트롤러가 서비스 빈을 생성자 주입으로 받아 사용하는 모습이 전형적인 합성이고, DI는 이런 합성을 쉽게 만들기 위한 메커니즘이다.

반대로 상속은 구현 재사용을 노리다 보면 부모와 자식 클래스가 강하게 결합되기 쉽고, 부모의 내부 구현에 대한 이해를 전제로 하게 되어 캡슐화가 약해질 수 있다. 또한 클래스 계층은 컴파일 시점에 고정되므로, 이미 생성된 객체의 행동을 다른 계열로 바꾸려면 새로운 인스턴스를 만들어 교체해야 한다.

예를 들어 Movie와 DiscountPolicy의 관계가 지금처럼 합성이 아니라 상속으로 엮였다면, 정책을 바꾸기 위해서는 새로운 하위 타입을 만들거나 기존 객체의 상태를 복사해 새 인스턴스로 갈아 끼워야 한다. 반면 합성에서는 DiscountPolicy 인터페이스에 의존하고, 생성자나 세터를 통해 구현체를 갈아 끼우기만 하면 된다. 또한, 인터페이스로 메시지(연산)만 노출하므로 캡슐화도 지켜진다.

 

정리하면, 재사용과 변화 대응 관점에서는 합성을 기본값으로 선택하는 것이 올바르다. 그렇다고 상속을 배제하는 것은 아니고, DiscountPolicy처럼 인터페이스를 상속해 다양한 구현을 제공하고, 그 구현들을 합성으로 사용하는 쪽(Movie)이 의존하는 구조(상속(타입 확장) + 합성(사용)의 조합)가 가장 유연한 설계다. 

 

추상화 그리고 유연성

추상화라는 단어는 처음 들으면 정말 말 그대로 추상적이라고 생각이 든다. 그래도 개발 공부를 하며 자주 듣고 또 예시도 많이 봐오면서 추상화라는 단어의 의미는 어느 정도 익숙해진 것 같다. 인터페이스로 구현체의 메시지를 추상화하면 장점이 무엇일까? 

 

첫 번째는 요구사항을 서술할 때, 더 이해하기 쉬운 문장으로 서술할 수 있다는 점이다. 그에 따라 도메인 모델을 표현할 때도 더 간단하게 표현할 수 있다.

추상화를 적용하지 않는다면 모든 인터페이스의 구현체를 위와 같이 표현해야 한다. 이는 전체적인 구조를 파악하는데 어려움을 제공해 주는 요소이다. 위에서 말한 추상화의 장점은 이런 간단한 모델링을 진행할 때 아래와 같이 표현해 볼 수 있다는 것이다.

두 번째는, 설계의 유연함이다. 할인 정책이 없는 영화의 경우 나는 보통 조건문을 이용하여 예외케이스로 다루었다. 그 이유는, 당장 해결하기 쉬운 방법이고 이것을 추상화를 이용하여 해결할 수 있다는 생각까지 갈 수 없었다. 하지만, 이 책에서는 할인 정책이 없는 경우를 위해 할인 정책 인터페이스를 구현한 NoneDiscountPolicy를 만들고 이를 이용하여 기존 로직에 대한 변경 없이 기능을 확장한다. 이 과정을 보며 아 클래스 수를 늘려서라도 추상화를 하는 이유는 이런 확장성 있는 개발을 하려고 하는 거구나 느낄 수 있었다. 지금까지 해왔던 개발도 사실은 조금만 비틀어서 추상화라는 기술을 생각했더라면 더 좋은 개발로 발전할 수 있지 않았을까?

 

물론 추상화도 만능은 아니다. 새로운 클래스를 만들어서 기능을 확장하면 기존에 존재하던 기능과 결합되는 부분이 나오고 이를 또 분리하기 위해 새로운 인터페이스를 만들다 보면 이성적으로 좋은 설계로 나아가더라도 현실적으로 가독성 측면에서 이해하기 힘든 설계 구조가 탄생하게 된다. 따라서, 항상 사소한 결정이라도 "객체지향 패러다임에 따르면 이렇게 해야 해"가 아니라 팀 내의 토론을 거쳐 "이런 이유로 이런 결정을 하게 됐다"는 결론을 내리는 과정이 필요한 것 같다.

책에서는 이렇게 말한다. "고민하고 트레이드오프해라"

 

이 부분을 읽으며, 코드를 작성할 때, 추상화도 중요하지만 추상화는 결국 가독성을 떨어트릴 수 있다는 사실도 인지하며 개발해 나가야 한다고 생각이 들었다.

 

마무리하며..

이번 글에서는 개념적인 부분과 지금까지 내가 어떻게 학습해 왔는지 정리해 보았다. 나름 객체지향 패러다임을 익숙하게 사용하기 위해 노력하고 있는데 아직도 많이 부족한 것 같다. 다음 글에서는 객체지향 패러다임을 익히기 좋은 미니 프로젝트를 하나 선정해서 이 글에서 작성한 방식들을 적용해 코드를 작성하고 누군가에게 코드리뷰를 받아보는 과정까지 진행해 봐야겠다.