SOLID 원칙을 이론적으로 배우긴 했지만 실제로 코드를 작성할 때엔 항상 내가 구성한 이 아키텍처가 정말로 객체지향적인지, SOLID 원칙을 잘 만족했는지 의구심이 들었다.
문득 내가 객체지향에 대해 제대로 이해하지 않은 채로 객체지향적인 코드를 작성하려고 하고 있다는 생각이 들었다.
객체지향에 대한 인사이트를 얻고자 객체지향의 사실과 오해라는 책의 저자 조영호님이 2024 인프콘에서 강의한 객체지향은 여전히 유용한가?를 시청하고 내용을 정리하였다.
장바구니에 할인을 해주는 프로모션 도메인을 예시로 들어 변화하는 요구사항의 종류에 따라 절차지향 방식과 객체지향 방식이 어떻게 대응하는지 보여주어 언제 객체지향을 도입해야 하는지 설명하는 것이 이 강의의 내용이다.
장바구니의 전체 금액이 프로모션의 기준 금액보다 크면 할인을 적용하여 시스템의 상태를 변화하는 로직을 절차지향 방식과 객체지향 방식으로 비교해보자.
절차지향 설계는 프로세스(할인 여부를 결정하는 로직)와 데이터가 분리된 공간에 존재한다.
반면 객체지향 설계는 할인 여부를 결정하는 로직이 프로모션 클래스 안에 데이터와 함께 존재한다.
적절한 객체에게 책임을 위임하고 메시지를 통해 객체간의 협력으로 문제를 푸는것이 객체지향이다.
기존에는 Cart 전체 금액 >= Promotion 기준 금액이면 할인해주는 로직이었다.
만약 할인 조건을 Promotion 최소 금액 <= Cart 전체 금액 <= Promotion 최대 금액 이런식으로 변경하기 위해 Promotion 데이터의 변경이 발생한다면?
데이터를 바꾸면 그 데이터에 의존하는 모든 프로세스가 같이 변경된다. (결합도가 높다.)
객체지향 방식은 데이터를 사용하는 로직 부분이 같은 클래스 내에 있으므로 프로모션 클래스의 코드만 바꾸면 해결된다.
데이터와 프로세스가 한 곳에 있기 때문에 데이터가 변경되면 해당 클래스만 변경하면 된다. (결합도가 낮다.)
이번에는 장바구니에 담긴 총 수량을 기준으로 할인 여부를 판단하는 새로운 타입이 추가된다면?
절차지향 방식은 우선 새로운 할인 조건(타입)이 들어갈 수 있는 공간을 확보하고 수량으로 할인 여부를 체크하는 로직을 추가하게 된다.
보통 Enum으로 ConditionType을 Promotion에 추가하고, 프로세스가 다양한 할인 조건을 커버할 수 있도록 if문 혹은 switch문을 사용하는 식으로 구현한다.
즉, 조건이 추가되는 경우에도 데이터와 프로세스 코드가 같이 변경되어야 한다.
객체지향 방식에선 가격을 통해 할인 여부를 판단하는 조건과 수량을 통해 할인 여부를 판단하는 조건이 Promotion 클래스 입장에선 동일하게 할인이 가능한지를 판단하는 일을 하므로,
이렇게 동일한 일을 하지만 구현방법이 다른 여러가지 조건을 동일한 타입으로 묶어줘서 다형성을 활용한다. (하나의 추상화)
가격 조건과 수량 조건은 둘 다 할인 여부를 판단한다는 동일한 기능을 수행한다. 이를 추상화하기 위해 isApplicableTo 라는 메소드를 갖는 DiscountCondition이라는 인터페이스를 만들어 준다.
가격 조건과 수량 조건은 이 DiscountCondition의 isApplicableTo라는 메소드를 각자의 조건에 맞게 세부 구현해주면 된다.
객체지향의 특징은 절차지향 방식과 달리 로직에 사용되는 데이터들이 적합한 클래스로 분배가 된다는 점이다.
나의 경우 절차지향 방식의 코드 수정은 머리속에서 자연스럽게 떠올랐는데, 객체지향 방식은 익숙하지 않았다. 여기서 나는 그동안 절차지향 방식에 익숙해져 있었고 이러한 방식으로 코딩하고 있었다는 것을 느꼈다.
그렇다면 객체지향 방식이 절차지향 방식보다 훨씬 할 일이 많아지는것 같은데 그럼에도 불구하고 객체지향을 선택하는 이유가 뭘까?
추상화 과정은 많은 고민이 들어가고 익숙지 않다면 오랜 시간이 필요하지만, 한번 이렇게 시스템을 만들어 놓는다면 위 상황과 같이 타입(이 경우 할인 조건)의 확장이 일어나도 기존 코드를 수정하지 않고 새로운 클래스를 추가하여 해결할 수 있다.
할인 조건이 계속해서 추가되더라도 DiscountCondition이라는 인터페이스를 구현하는 방식으로 구조가 강제되기 때문에 일관성 있는 설계가 가능해진다.
설계의 강제가 나쁘다고 생각할 수도 있지만, 나 혼자 작업하는게 아닌 여러 사람이 작업하는 환경에서 이러한 일관적인 룰이 있는것이 모든 팀원이 코드의 구조를 이해하는데 훨씬 수월하다고 생각한다. 프레임워크가 존재하는 이유도 그런것이니...
여기서 장바구니에 있는 하나의 항목을 가지고 할인 여부를 판단할 수 있는 기능을 추가하는 상황을 가정해보자. 장바구니에 들어가는 각 항목을 CartLineItem이라는 클래스로 표현한다.
우선 할인 여부를 판단하는 조건으로 가격 조건만 있는 경우를 생각해보자.
절차지향 방식에선 PromotionProcess라는 할인 여부를 판단하는 로직을 갖는 프로세스에 장바구니의 특정 항목을 가지고 할인 여부를 판단할 수 있는 메소드를 추가하면 된다.
그렇다면 앞서 확인한 것 처럼 수량을 바탕으로 할인 여부를 판단하는 조건이 추가된다면 어떻게 될까?
절차지향 방식의 경우 동일하게 CartLineItem으로 할인 여부를 판단하는 메소드만 추가하면 된다.
발표 자료에서 item 대신 cart로 오타가 난것 같다.
할인 여부 판단 조건인 ConditionType이 계속 새롭게 추가되는 상황이라면 프로세스의 case 문을 매번 수정해줘야 하므로 좋지 않은 설계가 되지만, ConditionType이 새롭게 추가되지 않고 기능만 추가되는 상황이라면 절차지향 방식이 좋다.
반면 객체지향 방식의 경우 DiscountCondition 인터페이스에 CartLineItem을 사용하는 새로운 메소드를 정의해주고, 이를 구현하는 모든 구현체에 해당 메소드를 구현해줘야 한다.
따라서 현재 비즈니스 상황이 타입이 확장되는 경우가 많은지, 아니면 타입은 거의 고정이고 새로운 기능만 추가되는 상황인지 잘 고려하여 아키텍처를 설계해야 한다.
데이터를 변환해야 하는 경우에도 절차지향이 객체지향보다 유리할 수 있다.
Cart와 Promotion의 데이터를 조합해서 가공 후 CartWithPromotion이라는 데이터를 출력해야 하는 상황이라고 생각해보자.
절차지향 방식은 단순히 프로세스에서 데이터들을 가져와서 가공 후 CartWithProcess에 넣어주기만 하면 된다.
반면 객체지향 방식은 앞서 확인한 것처럼 데이터들이 적합한 클래스로 분배가 되어 있기 때문에(basePrice 는 PriceCondition에, baseQuantity는 QuantityCondition에)
다루고 있는 객체가 어떤 타입인지 확인하고 타입 캐스팅하여 적절한 데이터를 꺼내는 과정이 필요하므로 절차지향 방식보다 번거롭다.
정리하면, 타입이 확장되는 것 보다는 기능이 추가되는 상황이고, 데이터를 처리하는 로직이 들어간다면 절차지향 방식이 유리할 수 있다.
절차지향 설계는 데이터를 한군데에 모으지만, 객체지향 설계는 데이터를 사용하는 로직(행동)에 따라 분배한다. 따라서 내가 작성해야 할 로직이 데이터 중심이라면 절차지향, 행동 중심이라면 객체 지향으로 설계하는 것이 좋다.
행동 중심이라는 것은 각 타입들의 행동(메시지, 메소드)은 같지만 이 행동을 구체적으로 구현하는 방식이 다를 때를 말한다. 강의에선 할인 조건은 각각 다르더라도 결국 이들의 행동은 모두 할인을 적용할 지 판단하는 것으로 같기 때문에, 이런 경우 다형성을 활용해서 객체지향적으로 설계하는 것이 좋다고 말한다.
이렇게 특정 상황에 따라 객체지향이 유리할 수도, 절차지향이 유리할 수도 있기 때문에 조영호 님은 특정 기술이나 패러다임이 언제 유용한지를 고민해서 적용해야 한다고 강조한다.
그래서 객체지향이 유용한가?가 의심되어 배우지 않는 것 보다는 일단 배워서 객체지향이 적합한 상황이 주어졌을 때 이를 써먹을 수 있도록 하는 자세를 가져야 한다.
나는 그동안 절차적으로만 코드를 작성하고 있었다는 것을 깨달았다.
객체지향 언어인 자바를 사용해서 코드를 작성하니까 객체지향적으로 코드를 작성하고 있다고 생각했는데, 아니었다. 알고리즘 문제를 푸는 방식으로 코딩을 하는게 익숙했던 나는 도메인 클래스들이 자율성을 갖고 각각의 책임을 지니기 보단 단순히 데이터를 들고 있는 것에 가깝게 설계하였다.
도메인 객체들이 데이터만 들고 있지 책임과 역할을 갖고 있지 않았기 때문에 이들은 수동적으로 서비스 레이어가 데이터를 꺼내가고 수정하도록 기다릴 수 밖에 없도록 설계했던 것 같다.
변경의 방향성에 초점을 맞춰 내게 필요한 아키텍처가 어떤 것인지 고민하여 적용해야 한다는 것을 배웠다.
"변하지 않는 것은 모든 것은 변한다는 사실뿐이다" 라는 말처럼 서비스의 요구사항은 항상 변화한다. 우리가 아키텍처에 대해 고민하는 이유도 이러한 요구사항의 변화에 민첩하고 정확하게 반영하기 위해서이고, 이를 위한 지침이 개방 폐쇄 원칙이다.
강의의 요지는 무조건 객체지향이 절차지향보다 더 우수한 아키텍처라는 것이 아니라, 변경의 방향성에 따라 객체지향이 더 변경을 잘 반영할 수 있기 때문에 이러한 사항을 잘 고려하여 상황에 맞게 적용해야 한다는 것이지만, 나는 애초에 객체지향에 대해 제대로 이해하지 못하고 있었기 때문에 객체지향적으로 코드를 작성해야 하는 상황에서도 그러지 못했을 거라는 생각이 들었다.
이때 변경의 방향성에 초점을 맞춰 내게 필요한 아키텍처가 어떤 것인지 고민하여 적용해야 한다는 것을 배웠다.
나는 그동안 사이드 프로젝트 구현 후 운영을 하지 않았기 때문에 어떻게 코드를 작성해야 변화하는 요구사항에 유연하게 대처할 수 있을 지, 즉 아키텍처에 대해 깊게 고민할 필요가 없었던 것 같다. 처음 기획한 기능만 제대로 동작하는 지 확인하면 끝났기 때문이다.
그러나 실제 서비스를 운영하게 되면 사용자의 요구사항에 따라 코드는 계속해서 변화해야 하므로, 아키텍처를 나의 비즈니스 상황에 맞게 잘 구성했는지가 중요해진다.
서비스 기업들이 개발자를 채용할 때 실제 사용자가 있는 서비스를 운영해본 경험을 높게 사고 이를 요구하는 이유도 변경에 유연하게 대응할 수 있는 아키텍처에 대한 고민을 했는지 확인할 수 있기 때문인것 같다. (물론 1차적으로는 운영 과정에서 발생할 수 있는 다양한 문제들에 대한 트러블 슈팅 경험이 있는지를 확인하는 것이 가장 크다고는 생각한다.)
나와 내 코드가 발전하기 위해선 앞으로는 여러가지 새로운 요구사항이 추가되는 시나리오를 가정해보고 이에 유연하게 대응할 수 있는 아키텍처가 어떤 것일지 고민해보는 자세가 필요하다고 느꼈다.