객체지향 설계와 도메인-주도 설계에 관심이 많으며 행복한 팀과 깔끔한 코드, 존중과 협력이 훌륭한 소프트웨어를 낳는다는 믿음을 가지고 있는 평범한 개발자입니다. 개발자, 교육자, 관리자를 오가며 익힌 다양한 경험을 바탕으로 좋은 코드와 함께 좋은 프로덕트를 만들기 위해 노력하고 있습니다.
저서로는 『객체지향의 사실과 오해』와 『오브젝트』가 있고 번역서로는 『엘레강트 오브젝트』가 있으며 『만들면서 배우는 클린 아키텍처』에 감수자로 참여했습니다.
💡개인블로그 : https://eternity-object.tistory.com/
강의
수강평
- 오브젝트 - 기초편
- 오브젝트 - 기초편
게시글
질문&답변
도메인의 범위
dev.taeyoung닌 안녕하세요. 좋은 질문 남겨 주셔서 감사합니다. 🙂 아래에 답변 드립니다.위와 같은 방식으로 도메인을 결정한다면, 인증을 위한 access token, refresh token 등은 도메인에 포함되지 않나요? 이런 기술적 구현체들은 실제 세계에는 없는 개념인데, 관련 모델과 서비스들을 어디에 배치하는 게 좋을까요?-> 질문 전에 말씀하신 것처럼 도메인은 "소프트웨어로 구현할 요구사항의 범위"입니다. 실세계에 있는지 없는지는 중요하지 않습니다. 그 문제가 우리가 해결해야 하는 요구사항 범위에 포함된다면 도메인에 포함됩니다. 예를 들어 게임을 만든다면 게임에 등장하는 요소들은 실세계에 존재하지 않는 것들이 많지만 이들은 게임을 만들기 위해서는 우리가 구현해야 하는 대상입니다. 해당 요소가 우리가 해결할 문제 영역에 포함된다면 도메인에 포함된다고 생각하시면 됩니다. 따라서 인증 역시 소프트웨어 안에 코드로 구현되어야 하기 때문에 도메인에 포함됩니다. 아마도 문제의 범위를 정의하는 '도메인'의 개념과 코드를 배치하기 위해 사용하는 '도메인 레이어'의 개념을 혼동하신 것 같아요. 인증이라는 요구사항을 코드로 해결해야 한다면 인증은 우리가 해결해야 하는 문제이기 때문에 도메인에 포함됩니다.반대로 서비스를 이용할 때 본인인증(kcb와 같은)이 필요한 상황이라면, 실제 세계에서의 신분증 검사 같은 방식의 신원 확인을 대체하는 Verification이라는 도메인이 생성되는 건가요?-> 본인인증이 필요한 상황이라면 이것 역시 우리가 해결할 문제이기 때문에 도메인에 포함됩니다. 'Verification이라는 도메인'이라고 말씀하신 것은 이번에는 '도메인'과 '도메인 개념'을 혼동하신게 아닌가 싶어요. 신원확인이라는 요구사항을 구현해야 한다면 신원확인을 구현하기 위한 요소들은 우리의 도메인에 포함됩니다. 그 중에서 신원 확인을 위한 도메인 로직이 존재한다면 (도메인의 유형에 따라 정확한 명칭은 다르겠지만) Verification이라는 도메인 개념을 반영하는 도메인 객체가 생성되어야 할 것이고 결과적으로 그에 대응되는 도메인 클래스가 만들어질 것입니다. 그리고 이 클래스들은 도메인 레이어에 배치될 것입니다.답변이 되었는지 모르겠네요. 🙂 감사합니다.
- 1
- 2
- 69
질문&답변
단일책임원칙과 응집도
안녕하세요.응집도(Cohesion)는 변경이 발생했을 때 클래스(또는 모듈)이 함께 변경되는 정도를 의미하고 단일 책임은 원칙(Single Responsibility Principle)은 클래스의 응집도를 높이기 위해서는 클래스(또는 모듈)이 동일한 이유로 함께 변경되도록 만들어야 한다는 설계 원칙입니다.답변이 되었는지 모르겠네요. 😃
- 1
- 2
- 58
질문&답변
예제코드 github
석재현님 안녕하세요.강의 섹션 중에 예제 코드가 제공되는 강의의 경우에는 화면 아래 쪽에 있는 '수업 노트 보기'를 클릭하시면 github 주소가 표시되어 있습니다.마지막까지 다 완강하시길 빌겠습니다.감사합니다. 🙂 (사진)
- 1
- 2
- 90
질문&답변
가격 필드가 Long fee가 아닌 Money fee가 된 계기가 궁금합니다.
Jinyoung Choi님 안녕하세요.강의가 도움이 되셨다니 다행이네요.좋은 질문 남겨 주셔서 감사합니다. 🙂 Money 클래스를 사용할 때 얻을 수 있는 이점은 아래 인프런 AI 인턴의 답변을 참고해 주시면 좋을것 같아요.일반적으로 Money와 같은 클래스를 도입하는 이유는 Long과 같은 원시형 타입으로는 표현하려는 개념을 명확하게 드러낼 수 없을 때 사용합니다.예를 들어서 다음과 같은 클래스가 있다고 생각해 보겠습니다.class AnyClass { private Long amount; private Long distance; public void increase(Long amount, Long distance) { this.amount += amount; this.distance += distance; } }위 클래스에서 amount와 distance는 타입은 Long으로 동일하지만 의미는 완전히 다릅니다.하나는 금액을 의미하고 다른 하나는 거리를 의미합니다.따라서 다음과 같이 클래스를 추가해서 의미를 명확하게 코드 안에 표현하는 것이 더 좋은 방법입니다.class AnyClass { private Money amount; private Distance distance; public void increase(Money amount, Distance distance) { this.amount = amount.plus(amount); this.distance += distance.add(distance); } }원래의 클래스는 둘 다 Long 타입이기 때문에 파라미터를 잘못 넘기더라도 컴파일 타임에 체크를 하기 어렵습니다.예를 들어서 AnyClass 인스턴스에 1000원을 더하고 거리에 10km를 더하고 싶다면 아래와 같이 메서드를 호출할 것입니다.anyClass.increase(1000, 10);프로그래머가 아래와 같이 두 값을 바꿔서 보내더라도 컴파일러는 오류를 체크할 수 없습니다.anyClass.increase(10, 1000);하지만 수정 후에 아래와 같이 메서드를 호출하면 타입이 다르기 때문에 컴파일 에러가 발생하게 됩니다.따라서 더 안정적으로 코드를 작성하고 유지보수할 수 있게 됩니다.anyClass.increase(Distance.of(10), Money.wons(1000));일단 Money나 Distance와 같은 클래스가 있다면 금액이나 거리를 계산하는 코드가 중복될 경우 이 클래스드로 이동시켜 중복 코드를 제거할 수 있기 때문에 재사용성도 높아집니다. 개념을 명확하게 표현하고 싶을때마다 이러 작은 클래스를 만드시면 됩니다.이런 작은 클래스들을 값 객체라고 부릅니다. 값 객체에 대한 더 자세한 내용은 아래 블로그를 참고해 주시면 감사하겠습니다.https://eternity-object.tistory.com/2 보시면서 궁금한 내용이 있으면 언제라도 질문 주세요. 🙂 감사합니다.
- 2
- 2
- 95
질문&답변
할인 조건의 구현에 대해
qwerty143님 안녕하세요. 강의가 도움이 되신다니 다행이네요. 🙂 좋은 질문 남겨주셔서 감사합니다. Screening의 isSequece 메서드와 SequenceCondition의 isSatisfiedBy 메서드는 책임의 관점에서 의미가 다릅니다. Screening의 isSequence 메서드는 회차가 동일한지를 판단합니다. Screening의 입장에서 isSequence 메서드는 할인 여부와는 상관이 없습니다. SequenceCondition의 isSatisfiedBy 메서드는 할인을 적용할 수 있는지 여부를 판단합니다. 회차가 동일할 경우 할인을 제공하기 때문에 Screening의 isSequnce 메서드를 재사용하고 있을 뿐입니다. 이런 의미적인 차이는 회차를 이용한 할인 여부 로직을 수정할 때 좀 더 명확해 지는데요만약 특정한 요일의 특정 회차에만 할인을 적용해야 한다면 다음과 같이 SequenceCondition의 isSatisfiedBy 메서드를 수정하게 될것입니다.아래 코드를 Screening에 구현하게 되면 Screening의 응집도가 낮아지게 됩니다.public class SequenceCondition implements DiscountCondition { private int sequence; private DayOfWeek dayOfWeek; @Override public boolean isSatisfiedBy(Screening screening) { return screening.isSequence(sequence) && screening.getStartTime().getDayOfWeek().equals(dayOfWeek); } } screening.getStartTime().getDayOfWeek(dayOfWeek)처럼 객체의 내부 속성을 연쇄적으로 가져오면 디미터 법칙(Law of Demeter)을 위반하기 때문에 객체의 인터페이스가 너무 복잡해지지 않는다면 screening.isScreenedOnDayOfWeek(dayOfWeek)과 같이 수정하는 것이 더 좋은 방법이기는 합니다. 답변이 되었는지 모르겠네요. 🙂
- 1
- 2
- 77
질문&답변
generic 패키지 money 클래스 관련 질문
황설탕님 안녕하세요.좋은 질문해 주셔서 감사합니다.말씀하신 것처럼 강의 내용을 확실하게 이해하는 가장 좋은 방법은 실제로 코드를 작성하는데 적용해 보는 것 같아요. :)Money 클래스가 있는 generic 패키지는 보통 어떤 특성을 가지는 클래스들을 모아놓는지 궁금합니다.개인적으로 특정한 도메인이나 애플리케이션에 종속되지 않고 여러 애플리케이션에서 재사용할 수 있는 클래스들을 모아 놓습니다.Money 클래스는 금액이 필요한 어떤 애플리케이션에서도 재사용가능하기 때문에 generic 패키지에 위치시켰습니다. Money 클래스와 같은 역할을 하는 객체들 또한 행동을 정의한 후 객체를 선택하라의 원칙에 의해 행동을 정의 한 후 필드를 결정하는 것인지 궁금합니다.Money와 같은 유형의 객체는 일반적으로 클래스의 코드를 리팩터링하는 과정에서 함께 사용되는 필드들과 관련된 로직을 명시적인 개념으로 묶기 위해 사용됩니다.따라서 행동을 정의한 후 객체를 선택하는 방식과는 조금 다른 관점에서 설계가 진행됩니다.필드가 아니라 로직을 담는 클래스를 추가한다는 점에서 행동에 집중한다는 점은 동일하지만 협력을 설계하는 과정에서 행동을 먼저 결정하고 객체를 나중에 결정한다기 보자는 이미 구현된 로직을 옮겨 담을 객체를 추가한다는 개념으로 접근하는게 일반적입니다. Money 같은 성격의 클래스는 어떤 서비스를 설계하기 전 전 미리 작성한 후 서비스 설계를 해야하나요?2번 질문에서 답변드린 것처럼 일반적으로 기존 코드를 리팩터링하는 과정에서 Money와 같은 유형의 객체가 추가됩니다.물론 Money처럼 여러 도메인에 걸쳐 사용될 수 있는 클래스라면 이미 구현되어 있는 클래스를 재사용하기도 하겠지만 도메인에 특화된 객체나 존재하지 않는 경우에는 구현을 하면서 식별되는 것이 일반적입니다. Money 는 새 불변 객체를 만들어 리턴해주는 방식으로 사용하던데 Screening 등의 클래스에서는 불변 객체를 사용하지 않는 이유가 있는지, 있다면 특정 클래스를 불변 객체로 설계하는 기준이 있는지 궁급합니다.Money는 값 객체(Value Object)에 속하고 Screening은 참조 객체(Reference Object)에 속합니다.값 객체는 참조 객체의 속성을 표현하기 위해 사용되며 불변으로 만드는 것이 일반적입니다.값 객체와 참조 객체의 개념과 용도에 대해서는 제 블로그에 있는 시리즈 글을 참고하시면 쉽게 이해하실 수 있으실 거에요. :)https://eternity-object.tistory.com/2답변이 되었는지 모르겠네요. 🙂 감사합니다.
- 1
- 2
- 74
질문&답변
DiscountCondition을 DiscountPolicy 뒤에 숨겨야 하는 이유
ttt123님 안녕하세요.좋은 질문 남겨 주셔서 감사합니다.현재 코드 구조를 그려보면 아래 그림처럼 Movie는 추상클래스인 DiscountPolicy에만 의존하고 있습니다.(사진) DiscountPolicy의 서브클래스들과 DiscountCondition은 추상클래스인 DiscountPolicy 내부에 숨겨져 있기때문에 Movie의 입장에서는 접근할 수가 없습니다.다시 말해서 Movie의 입장에서는 DiscountPolicy 추상클래스만 보이고 다른 클래스들은 보이지 않는 것이죠.이 개념은 캡슐화(Encapsulation)로 연결되며 Discountpolicy 추상클래스만 변경하지 않는다면 Movie는 영향을 받지 않게 됩니다.결과적으로 DiscountPolicy 클래스를 기준으로 구조를 변경하더라도 Movie에는 영향을 미치지 않는 것이죠.(사진) 만약 Movie가 DiscountPolicy와 DiscountCondition 모두에 의존한다면 변경에 의해 더 자주 영향을 받게 되었을 것입니다.변경의 영향을 최소화하기 위해 DiscountCondition을 DiscountPolicy 뒤로 숨겼다고 생각하시면 될것 같아요. 6-3. 결합도와 6-4. 캡슐화를 보시면 더 깊이 있게 이해하실 수 있으실거에요. 🙂 보시고 궁금한 점 있으면 또 질문 남겨 주세요!
- 2
- 3
- 129
질문&답변
영화 예매 도메인 코드 작성
안녕하세요.정확하게 질문하신 어떤 부분이라는게 무엇을 의미하시는지 알 수 없어 일단 제가 생각하는 답을 드리도록 할게요.혹시 질문하신 의도와 거리가 있다면 추가적으로 질문을 남겨 주시면 감사하겠습니다. 🙂 기능을 구현하는 순서는 크게 두 가지로 구분할 수 있습니다.Outside-In : 프리젠테이션 레이어부터 도메인 로직을 향해 코드를 완성해가는 방법Inside-Out: 도메인 로직을 완성한 후 그 위에 서비스 레이어나 프리젠테이션 레이어를 얹는 방법 사람에 따라 선호하는 방법은 다를 수 있지만 제 개인적으로는 강의에서 보여드린 것처럼 Inside-Out 방식을 선호합니다.하지만 프로젝트를 할 때는 Inside-Out 방식을 무조건 고수하기 보다는 상황에 적절한 방식을 선택하게 됩니다.도메인 로직을 먼저 구현하지만 상황에 따라 유연하게 작업 순서를 조정한다고 생각하시면 될것 같아요. 🙂감사합니다.
- 0
- 2
- 124
질문&답변
안녕하세요. 기존의 추상화된 역할에 대해서 새로운 협력자가 필요하게 되는 경우는 어떻게 설계해야할까요??
윤철님 안녕하세요.좋은 질문 남겨주셔서 감사합니다. 🙂추상화의 역할은 '현재 알고 있는' 변경을 캡슐화해서 코드 수정으로 인해 받을 수 있는 영향을 최소화하는 것입니다.여기에서 '현재 알고 있는' 변경이라는 점이 중요한데 어떤 부분이 변경될지 모르는 상황에서 예상에 기반해서 설계할 경우 불필요한 추상화를 도입하게 되고 결과적으로 코드를 수정하게 되기 때문입니다.현재 알고 있는 변경은 Screening의 데이터에 따라 할인 여부가 달라진다는 사실이기 때문에 Screening에 의존하고 있지만 그 시점에 어떤 데이터가 필요할지 알지 못하기 때문에, 그리고 Screening이 아닌 다른 요소들에 기반해서 할인 여부를 판단하도록 요구사항이 변경될지 알지 못하기 때문에 현재와 같은 클래스 구조를 유지했다고 생각하시면 될 것 같아요.실제로 Screening 이외의 데이터를 필요로하지 않는다면 현재 코드는 그 사실을 명확하게 보여줍니다.실제로 코드가 변경되고 새로운 추상화가 필요하다면 그 사실을 코드에 반영하면 됩니다.말씀하신 경우에는 객체가 아닌 가격 계산에 필요한 항목들을 포함하는 새로운 객체를 추가해서 해결할 수 있을 것 같습니다.public class PriceFactors { private Screening screening; private Customer customer; public PriceFactors(Screening screening, Customer customer) { this.screening = screening; this.customer = customer; } public Money getFixedFee() { return screening.getFixedFee(); } public boolean isSequence(int sequence) { return screening.isSequence(sequence); } public LocalDate getBirthdate() { return customer.getBirthdate(); } }그리고 Movie, DiscountPolicy, DiscountCondition의 오퍼레이션이 PriceFactors를 전달받도록 수정합니다.public interface DiscountCondition { boolean isSatisfiedBy(PriceFactors factors); }이제 BirthdateCondition을 다음과 같이 구현할 수 있습니다.public interface BirthdateCondition implements DiscountCondition { @Override public boolean isSatisfiedBy(PriceFactors factors) { return factors.getBirthdate().equals(LocalDate.now()); } }새로운 요구사항으로 인해 많은 클래스를 수정해야 하기 때문에 변경 전의 코드는 Screening 이외의 다른 요소를 이용해서 할인 여부 확인에 대해서는 취약한 설계라고 할 수 있습니다. 여기에서 긴장은 PriceFactors를 프로젝트 처음부터 추가할 수 없었느냐인데 새로운 할인 대상이 추가된다는 사실이 확정이 아닌 상태에서 기존 코드에 PriceFactors를 추가했다면 코드가 불필요하게 복잡해 보일것입니다. 다른 관점에서 Screening에 대해 모든 클래스가 의존하기 때문에 PriceFactors를 미리 추가하는 것이 도움이 된다고 생각할 수 있습니다. 이 경우 PriceFactors를 미리 추가하는 것이 더 합리적이라고 볼 수 있습니다.결국 PriceFactors와 같은 새로운 요소를 미리 도입할지 여부는 해당 변경에 대한 발생가능성과 발생했을 때 코드를 수정하기가 얼마나 어려운지에 따라 달라지게 됩니다.이제 PriceFactors를 이용해서 리팩터링했기 때문에 현재의 설계는 이런 유형의 변경을 안정적으로 추가할 수 있게 됐습니다. 하지만 현재 예상할 수 없는 새로운 유형이 요구사항이 추가된다면 리팩터링된 코드 역시 다시 수정될 수 밖에 없습니다.여기에서 눈여겨보실 부분은 Screening, Movie, DiscountPolicy, DiscountCondition의 역할과 협력 방식은 변경되지 않았다는 점입니다. 현재의 설계는 인터페이스는 변경되지만 객체의 역할과 책임 관점에서는 안정적이라고 할 수 있습니다.다음과 같이 정리할 수 있을것 같아요.예상하지 못했던 새로운 유형의 요구사항이 추가됐을 때 변경하지 않고 해당 요구사항을 수용할 수 없습니다. 현재의 요구사항에 적합한 추상화를 선택하세요.요구사항이 변경된다면 변경된 요구사항에 적합한 추상화를 이용해서 코드를 수정하세요. 하지만 예측에 기반해서 추상화를 도입하지 말고 실제로 변경이 일어날 때까지 기다렸다가 적합한 추상화를 도입하세요.요구사항이 너무 복잡해져서 현재의 추상화에 적합하지 않다면 전체적인 역할, 책임, 협력을 수정해야할 수도 있습니다. 현재의 추상화를 버리고 변경된 요구사항에 적합한 새로운 역할, 책임, 협력을 찾으세요.질문 중에 테스트가 매번 깨지는 상황이 발생할 수 있다고 하신 부분이 있는데 이건 아마도 테스트가 실패한다는 의미가 아니라 테스트 코드를 수정해야 하는 상황을 말씀하신 것 같아요. PriceFactors를 도입한 후에는 Customer와 Screening과 같은 요소에 기반해서 할인 정책이 변경되는 경우에는 테스트 코드를 수정할 필요가 없을 거에요. 하지만 기존에 알지 못했던 새로운 유형의 요구사항이 추가된다면 당연히 테스트 케이스는 수정될 수 밖에 없을 것입니다.정말로 코드 수정에 의해 테스트가 매번 수정된다면 3번과 같이 현재의 역할, 책임, 협력을 다시 고민해야 하는 좋은 기회로 삼을 수 있을것 같아요.요약하면 추상화와 캡슐화는 현재 알고 있는 변경을 드러낼 수 있는 것이면 됩니다. 미래의 요구사항은 알 수 없기 때문에 예상에 기반해서 불필요한 추상화를 미리 도입해서는 안됩니다. 대부분의 경우에 역할과 책임을 납득할만하게 할당하고 클래스와 메서드를 작게 유지한다면 생각보다 코드를 수정하기가 어렵지 않을거에요.답변이 되었는지 모르겠네요. 😄
- 3
- 2
- 118
질문&답변
도메인 추출 방법
JMJ님 안녕하세요. 좋은 질문 남겨 주셔서 감사합니다.애플리케이션 개발에 적합한 도메인 개념을 찾는 일은 저를 포함해서 모든 사람들이 어렵게 생각하는 작업이기 때문에 창피해 하실 일이 전혀 아닙니다.도메인 개념을 찾기 위한 다양한 패턴이나 카탈로그들이 제시되어 왔지만 모든 경우에 적용할 수 있는 최적화된 해법은 존재하지 않는 것으로 보입니다.애플리케이션마다 가정하고 있는 제약조건이 다르고 요구사항의 변화 양상이 다르기 때문에 어떤 도메인에 적합한 도메인 개념은 이것이다라고 말하기 어려운 것 같아요.다양한 자료를 보시고 많은 프로젝트 경험이 쌓일수록 조금 더 수월해지기는 하지만 아직도 여전히 어려운걸 보면 경험이 모든 것을 해결해 주지는 못하더라구요.제가 가장 쉽고 직관적이리고 생각하는 가이드는 다음과 같습니다.현재 시점에서 가장 단순하게 생각되는 개념들을 기반으로 우선 코드를 구현해봅니다.처음부터 모든 개념을 정확하게 추출하려고 하지말고 우선 생각나는 가장 간단한 개념들을 기반으로 코드를 작성하면서 고민했던 개념들이 현재 요구사항에 적합한지 최대한 빠르게 피드백을 받는 것이 좋습니다. 구현 시에 클래스가 응집도나 결합도 관점에서 너무 커지거나 복잡해진다면 새로운 개념을 추가한 후 일부 책임을 새로운 개념에 맡깁니다.이렇게 코드의 퀄리티를 높여야 하는 시점이 되면 도메인에 대해 좀 더 깊게 고민하게 되고 자연스럽게 새로운 도메인 개념이 도출되게 됩니다.요구사항이 변경되면 현재의 코드가 요구사항을 수용하기에 적합한지 판단하고 필요하면 새로운 도메인 개념을 기반으로 코드를 리팩터링합니다.요구사항이 변경되는 시점이야말로 새로운 도메인 개념을 추출할 수 있는 적절한 타이밍입니다. 지금까지 적합하지 않아 보이던 도메인 개념이 위치를 찾고, 적합해 보이던 도메인 개념이 여러 개의 더 작은 도메인 개념으로 나뉘기도 합니다. 리팩토링을 통해 새롭게 얻은 도메인 지식을 코드에 반영해야겠죠.도메인 개념이 고정된 것이 아니고 코드를 구현하거나 요구사항이 변경됨에 따라 지속적으로 변경된다는 사실을 의도적으로 인식하고 언제라고 리팩토링하신다면 적절한 시기에 적합한 도메인 개념들을 식별하실 수 있으실거에요.여기에 아래 인프런 AI가 제시하는 가이드가 나와있는 다양한 자료를 학습하시고 실제 프로젝트에서의 경험치를 쌓으신다면 좀 더 수월해지실 거라고 생각합니다. JMJ님뿐만 아니라 모든 분들이 어려워하시는 영역이고 한번에 적절한 도메인 개념들을 모두 식별하는 것은 어려운 작업이라서 지속적으로 리팩토링을 통해 적절한 도메인 개념을 식별한다는 마음으로 접근하시면 부담이 적어지실 거에요. 답변 중에 좀 더 세부적인 내용이 궁금하시면 질문해 주세요. 🙂 즐거운 주말 보내세요!
- 1
- 2
- 163