객체지향 설계와 도메인-주도 설계에 관심이 많으며 행복한 팀과 깔끔한 코드, 존중과 협력이 훌륭한 소프트웨어를 낳는다는 믿음을 가지고 있는 평범한 개발자입니다. 개발자, 교육자, 관리자를 오가며 익힌 다양한 경험을 바탕으로 좋은 코드와 함께 좋은 프로덕트를 만들기 위해 노력하고 있습니다.
저서로는 『객체지향의 사실과 오해』와 『오브젝트』가 있고 번역서로는 『엘레강트 오브젝트』가 있으며 『만들면서 배우는 클린 아키텍처』에 감수자로 참여했습니다.
💡개인블로그 : https://eternity-object.tistory.com/
강의
수강평
- 오브젝트 - 기초편
게시글
질문&답변
이 강의만 자료가 ppt네요.
ansxjrdptj94님 안녕하세요.캡슐화에 ppt 파일이 올라가 있었네요.파일을 수정해서 올릴 때 잘못 올린것 같아요.확인해 주셔서 감사합니다 🙂행복한 하루 보내세요.
- 1
- 1
- 26
질문&답변
Game, Player, Room간의 관계에 대한 질문을 드려요
테디베어님 안녕하세요.설계원칙편에서 다시 만나뵈니 반갑네요. 🙂 테디베어님이 주신 질문의 핵심은 화면에 출력할 문구를 구성하는 책임을 Player로 옮기는게 맞는가로 요약할 수 있습니다.예제 코드에서는 Game이 다음과 같이 Player로부터 정보를 얻은 후 문구를 구성하고 있습니다.public class Game { private Player player; private CommandParser commandParser; private boolean running; private void showGreetings() { System.out.println("환영합니다!"); } private void showHelp() { System.out.println("다음 명령어를 사용할 수 있습니다."); System.out.println("go {north|east|south|west} - 이동, look - 보기, help - 도움말, quit - 게임 종료"); } private void farewell() { System.out.println("\\n게임을 종료합니다."); } public void showRoom() { System.out.println("당신은 [" + player.currentRoom().name() + "]에 있습니다."); System.out.println(player.currentRoom().description()); } ... } 코드를 보시면 화면에 출력할 문구를 구성하는 책임을 Game 클래스가 담당하고 있다는 사실을 알 수 있습니다.나머지 클래스들은 화면에 출력할 문구를 조합하는데 필요한 데이터를 제공하는 책임을 담당하고 있습니다. 이렇게 두 가지 책임을 분리한 이유는 화면에 출력할 문구를 변경하는 것은 Player가 담당하는 로직과 상관없이 변경될 수 있기 때문입니다.즉, 변경의 이유가 다르기 때문입니다. 이를 관심사의 분리라고도 부르는데 화면에 출력할 문구는 UI와 관련된 관심사에 해당하고, Player의 로직은 게임과 관련된 도메인 관심사에 해당하기 때문입니다.따라서 UI의 변경으로 화면에 출력될 문구를 변경하고 싶은 경우에는 Player나 다른 객체들은 수정하지 않고 Game 클래스만 수정할 수 있도록 변경의 범위를 제한한 것으로 생각해 주시면 됩니다. 실무에서 어떤 객체를 데이터베이스에 저장하거나 조회하는 로직을 객체가 아니라 별도의 Repository나 DAO에 구현하는 경우와 유사하다고 생각하시면 될 것 같아요.예를 들어 Customer를 데이터베이스에 저장할 때 Customer의 필드를 이용해서 데이터베이스에 저장하지만 관심사 측면에서는 다르기 때문에 별도의 객체를 이용해서 객체의 데이터를 저장하는 방식을 떠올리시면 이해가 되실거에요.이렇게 객체 단위의 응집도를 낮추고 캡슐화를 약화시키는 대신 시스템 전체 관점에서 응집도를 높이는 기법이 자주 사용됩니다. 답변이 되었는지 모르겠네요.감사합니다. 🙂
- 1
- 2
- 43
질문&답변
DiscountPolicy의 구현체에 관련 질문 드려요!
테디베어님 안녕하세요좋은 질문 남겨 주셔서 감사합니다. 🙂Movie의 calculateFee 메서드와 DiscountPolicy의 getDiscountAmount 메서드는 서로 다른 이유로 Screening 객체에 의존하고 있습니다. 1. Movie 클래스가 Screening에 의존하는 이유말씀하신 것처럼 이 경우는 DiscountPolicy가 금액을 계산하기 위해 Screening의 데이터가 필요하기 때문에 이 데이터를 전달하기 위해 의존성이 필요합니다.만약 이 파라미터를 끊고 싶다면 아래와 같이 양방향 연관관계를 추가하는 방법이 있습니다.아래 코드를 보시면 DiscountPolicy에 Movie에 대한 참조를 추가해서DiscountPolicy가 직접 Movie를 통해 Screening에 접근하고 있습니다.public class Screening { private Movie movie; } public class Movie { private Money fee; private DiscountPolicy discountPolicy; **private Screening screening; public void calculateDiscount() { fee.minus(discountPolicy.calculateDiscount()); }** } public class DiscountPolicy { private DiscountPolicy policy; **private Movie movie; // Movie 참조 추가** public Money calculateDiscount() { for (DiscountCondition each : conditions) { if (each.isSatisfiedBy(**movie.getScreening()**)) { return getDiscountAmount(**movie.getScreening()**); } } return Money.ZERO; } abstract protected Money getDiscountAmount(Screening screening); } 이렇게 하면 Screening을 메서드의 파라미터로 전달할 필요가 없어지지만 아래 그림처럼 양방향 의존성이 추가되어 결합도가 높아집니다. (사진) 이렇게 바꾸더라도 여전히 Movie는 인스턴스 변수로 Screening을 포함하기 때문에 Movie에서 Screening에 대한 의존성을 제거할 수도 없습니다.여기에서는 DiscountPolicy로 Screening을 전달해야하기 때문에 Movie에서 Screening으로의 의존성을 제거할 수는 없습니다.대신 가장 결합도가 낮은 방식인 메서드 파리미터로 Screening을 전달하는 방식을 선택한 것입니다.Screening 대신 값 객체를 전달해서 Screening에 대한 의존성을 완전히 제거하는 방법이 궁금하시면 아래 질문을 참조해 주시면 감사하겠습니다. 🙂https://inf.run/kqT92 2. DiscountPolicy의 getDiscountAmount 메서드가 Screening에 의존하는 이유getDiscountAmount 메서드는 자식 클래스들이 가격을 계산하기 위해 공통으로 의존하게 되는 메서드의 시그니처를 정의합니다.이 경우에는 상속 계층에 속한 모든 자식 클래스들이 동일한 메서드를 오버라이딩해야 하기 때문에 다른 자식 클래스에서 필요하지 않더라도 특정한 자식 클래스가 필요로 한다면 동일한 객체를 전달할 수 밖에 없습니다.이런 이유로 Screening을 전달한 것이죠.위에서 링크로 달아놓은 값 객체를 전달하는 예제의 경우에는 getDiscountAmount 메서드에 Screening 대신 값 객체를 전달할거에요.그런데 질문을 받고 코드를 보니 Screening대신 Movie를 전달하는게 결합도 측면에서 더 좋았을 것 같네요. 😂 답변이 되었는지 모르겠네요.또 궁금한 부분 있으면 추가로 질문 남겨주시면 답변 드리겠습니다.
- 1
- 2
- 31
질문&답변
강의 자료 관련 질문입니다! (2-4. 절차에서 객체로)
테디베어님 안녕하세요.강의 자료를 보니 말씀하신 것처럼 findDiscountCondition 메서드의 가시성은 public으로 수정하는게 맞습니다.장표를 만들면서 실수로 그 부분까지는 확인을 못하고 넘어간 것 같네요.혹시 코드가 이상하다면 github에 올라가 있는 코드와 비교해 보시면 좋아요. 🙂https://github.com/eternity-oop/object-basic-02-04/blob/main/src/main/java/org/eternity/reservation/domain/DiscountPolicy.java동영상 부분이 수정하기 어려워서 고민인데 이렇게 질문 남겨 주시면 나중에라도 한번 모아서 오류 있는 부분을 수정하도록 하겠습니다. 꼼꼼하게 봐주셔서 감사합니다!
- 1
- 2
- 40
질문&답변
도메인 관련 질문이 있습니다!
테디베어님 안녕하세요.죄송하지만 질문 내용을 제가 정확하게 이해할 수 없어서 정확한 답변을 드리기 위해 몇 가지 질문을 드려야 할 것 같습니다. Q1. "영화 관람료"라는 객체를 추가하고 상영 객체가 의존한다고 하셨는데 구체적으로 영화 관람료가 맡은 책임은 무엇이고, 상영이 영화 관람료에 어떤 방식으로 의존(또는 협력)하는 것일까요? 이 부분을 이해해야만 답변을 드릴 수 있는데 의사 코드 수준으로라도 적어주시면 이해하는데 도움이 될 것 같아요. Q2. "영화 관람료"를 추가해서 상영이 할인 정책과 협력하면 지금 방식보다 어떤 점이 개선된다고 생각하시나요? 이 방식이 현실과 더 밀접하다고 하셨는데 어떤 점에서 그런지도 적어 주시면 좋겠습니다. 위 내용을 적어주시면 적어주신 내용 기반으로 정확한 답변을 드릴 수 있을것 같아요! 🙂
- 1
- 2
- 51
질문&답변
6-2 보호 로직 중복 이슈
mint.inhrdev님 안녕하세요. 다음 질문과 중복되어 답변 대신 링크로 대신합니다.https://inf.run/41yfx 답변 내용을 보시면 궁금하신 부분에 대해 명확하게 이해하실 수 있으실거에요. 🙂 질문 남겨 주셔서 감사하고 해당 답변을 읽으신 후에도 궁금한 부분이 있으면 추가로 질문 남겨주세요. 감사합니다!
- 1
- 2
- 39
질문&답변
3-2 메서드를 얼마나 작게 나누는게 적절한가요?
mint.inhrdev님 안녕하세요.좋은 질문 남겨주셔서 감사합니다. 먼저 SRP라는 용어는 메서드가 아니라 클래스 또는 모듈 단위에서 사용하는 용어입니다.메서드 수준에서는 단순히 응집도라는 개념을 사용하고 SRP라는 용어를 사용하지 않습니다.기본적으로 SRP에서 말하는 개념과도 상관이 없고 강의에서도 조합 메서드 패턴이라고 표현하고 SRP라는 용어는 사용하지 않고 있습니다. 조합 메서드로 리팩터링할 때 변경을 고민하기도 하지만 변경만이 메서드를 나누는 절대적인 기준은 아닙니다.강의에서도 설명드렸던 것처럼 조합 메서드의 목적은 추상화를 동일한 수준으로 맞춰서 읽고 이해하기 쉽도록 만드는 것이기 때문에 주된 목적은 가독성을 향상시키는 것입나다.따라서 질문을 SRP vs 가독성이라는 표현 대신 가독성을 향상시키기 위해 메서드를 얼마나 작게 나누는게 적절한가로 바꿔서 표현하는게 좋겠습니다. 먼저 질문에서 말씀하신 아래 코드는 “입력 파싱”이 아니라 공백을 기준으로 “토큰을 분리”하는 작업이라는 점을 짚고 넘어가야 할 것 같아요.input().toLowerCase().trim().split("\\\\s+"); 실제로 입력을 파싱하는 로직은 parseCommand() 메서드에서 처리하고 있습니다.private void parseCommand(String input) { String[] commands = input.toLowerCase().trim().split("\\\\s+"); switch (commands[0]) { case "go" -> { switch (commands[1]) { case "north" -> moveNorth(); case "south" -> moveSouth(); case "east" -> moveEast(); case "west" -> moveWest(); default -> showUnknownCommand(); } } case "look" -> showRoom(); case "help" -> showHelp(); case "quit" -> stop(); default -> showUnknownCommand(); } } 이제 질문은 위 코드를 아래처럼 변경하는게 parseCommand() 메서드 내부의 추상화 수준을 일관성 있게 만드는데 도움이 되느냐가 될겁니다.private void parseCommand(String input) { String[] commands = tokenize(input); switch (commands[0]) { case "go" -> { switch (commands[1]) { case "north" -> moveNorth(); case "south" -> moveSouth(); case "east" -> moveEast(); case "west" -> moveWest(); default -> showUnknownCommand(); } } case "look" -> showRoom(); case "help" -> showHelp(); case "quit" -> stop(); default -> showUnknownCommand(); } } private String[] tokenize(String input) { return input.toLowerCase().trim().split("\\\\s+"); } 아래 메서드가 전체적인 추상화 수준에서 일관성이 있어서 코드를 이해하기 쉽게 만든다고 판단된다면 두번째 코드처럼 분리하고, 해당 로직이 분리할 정도로 복잡하지 않거나 분리할 정도로 중요한 부분이 아니라면 그대로 두면 됩니다.제 개인적으로는 배열을 공백으로 분리하는 작업이 별도의 메서드로 분리할 정도로 중요하지 않고 parseCommand 자체가 switch 문을 이용해서 배열을 파싱하는 전체적으로 추상화 수준이 낮은 코드이기 때문에 메서드 전체의 추상화 수준에서는 이 상태로 둬도 괜찮다고 판단했습니다.만약 이 메서드를 동일한 추상화 수준으로 맞춘다면 아래 코드처럼 변경하는게 좋을겁니다.private void parseCommand(String input) { parseTokens(tokenize(input)); } private String[] tokenize(String input) { return input.toLowerCase().trim().split("\\\\s+"); } private void parseTokens(String[] command) { switch (commands[0]) { case "go" -> { switch (commands[1]) { case "north" -> moveNorth(); case "south" -> moveSouth(); case "east" -> moveEast(); case "west" -> moveWest(); default -> showUnknownCommand(); } } case "look" -> showRoom(); case "help" -> showHelp(); case "quit" -> stop(); default -> showUnknownCommand(); } } 결과적으로 paerseCommand() 메서드를 두 개의 작은 메서드를 호출하는 메서드로 나눌 것인가, 아니면 현재의 코드를 그대로 둘 것인가의 결정인데, 저 같은 경우에는 위 코드가 이전 코드보다 그렇게 개선된 것으로 보이지 않고 오히려 너무 세분화돼 보여서 파싱 흐름을 이해하기 어렵게 만들기 때문에 그대로 뒀다고 보시면 됩니다.만약 원칙에 따라 두 개의 작은 메서드로 나누는게 코드를 이해하기에 더 좋다고 판단된다면 나누셔도 좋습니다. 두번째로 언급하신 isRunning 메서드는 장표에 오류가 있고 코드에서는 누락되었네요.아래와 같이 isRunning() 메서드로 추출하는게 맞습니다. private boolean isRunning() { return running == true; } 너무 많은 장표를 만들다보니 확인하지 못한 부분이 많은데 확인해 주셔서 감사합니다.이 부분은 수정해서 커밋해 놓을게요. 🙂
- 1
- 3
- 49
질문&답변
영화, 상영, 예매 도메인 관계에 대한 질문
동관님 안녕하세요.좋은 질문 남겨 주셔서 감사합니다. 🙂먼저 영화와 예매는 N:M 관계가 아니라 1:N 관계입니다.어떤 관계에서 다중성(multiplicity)은 하나의 객체에 대해 연결될 수 있는 다른 객체의 개수를 나타냅니다.하나의 영화에는 여러 개의 예매가 만드어질 수 있는데 반해 예매는 하나의 영화에 대해서만 생성될 수 있기 때문에 영화:예매는 1:N 관계에 해당됩니다.상영은 그 자체로 도메인에 존재하는 중요한 개념입니다.아래 그림에서 영화는 "어쩔수가없다"이고 아래에 예매 가능한 4개의 박스가 상영에 해당됩니다.(사진) 참고로 다대다 관계를 해소하기 위해 중간 테이블을 추가하는 개념은 데이터베이스 모델링의 영역에 한정되는 기법입니다.객체 관계에서는 다대다 관계가 가능하기 때문에 다대다 관계를 해소하기 위해 중간 객체를 추가할 필요가 없고 협력 관점에서 특정한 동작을 수행할 객체가 필요하거나 도메인 관점에서 명시적으로 드러내야 하는 경우에 적절한 객체를 추가하게 됩니다. 답변이 되었는지 모르겠네요. 🙂감사합니다.
- 1
- 2
- 42
질문&답변
책임주도 설계 적용에 대한 간단한 질문 남겨드립니다.
Chanuk님 안녕하세요. 😊좋은 질문 남겨 주셔서 감사합니다.제가 아침부터 계속 강의를 하느라 이제서야 답글을 남기네요.답변이 늦어져서 죄송합니다. 현실적으로 DB 스키마가 이미 정해져 있거나, 기존 데이터를 마이그레이션해야 해서 새롭게 설계하기 어려운 경우, 또는 DBA가 별도로 관리하는 환경에서는 책임주도 설계를 적용하기가 쉽지 않을 것 같습니다. 이런 상황에서도 객체지향적인 설계를 현실적으로 적용할 수 있는 방법이 있을까요? (DAO를 중간 계층으로 두면 어느 정도 해결될까요? 아니면 도메인 레이어와 퍼시스턴스 레이어는 분리된 영역이니 크게 상관없을까요? 반대로, 두 레이어가 지나치게 달라지면 오히려 유지보수가 더 어려워지지는 않을까 하는 걱정도 듭니다.)근본적으로 DB 설계와 객체지향 설계는 접근방식과 목적이 다르기 때문에 신규 프로젝트라고 하더라도 두 구조 사이에 차이가 발생할 수 밖에 없습니다.이런 차이를 임피던스 불일치(impedance mismatch) 문제라고 부르는데 DB는 데이터 관점에서 중복을 최소화하는 방향으로 설계해야 하고 객체지향 설계는 강의에서 설명드렸던 것처럼 행동 관점에서 안정적인 구조를 만드는 것을 목적으로 하기 때문입니다. 임피던스 불일치 문제를 해결할 목적으로 만들어진 도구를 ORM(Object Relational Mapping)이라고 부릅니다. Java 진영의 JPA 표준과 Hibernate 구현체가 ORM의 대표적인 예라고 할 수 있습니다. ORM을 사용하면 직접 쿼리를 작성할 필요 없이 어느 정도 DB와 클래스 사이의 차이점을 상쇄시킬 수 있습니다. 물론 ORM은 만능이 아니기 때문에 모든 이슈를 다 해결해 주지는 못합니다. 따라서 실용적인 관점에서 완전한 객체지향 설계를 하려고 하시기 보다는 질문에서 언급하신 것처럼 매핑이 좀 더 수월한 방식으로 객체 구조를 선택하실 필요가 있습니다.ORM을 사용하는 경우에도 해결하기가 수월하지 않은 경우가 있는데 말씀하신 것처럼 레거시 테이블이 정규화가 안된 상태로 장기간 유지된 경우가 여기에 해당합니다. 이렇게 극단적으로 매핑이 어렵지만 객체지향 설계 방식을 선태하시는게 장점이 크다고 판단되시면 테이블과 1:1로 매핑되는 데이터 용 클래스를 만들어서 데이터베이스에서는 이 데이터용 클래스를 이용해서 데이터를 조회하신 후 객체 구조로 다시 매핑하는 방법도 있습니다. 이 방식은 테이블 스키마에 얽매이지 않고 객체지향 설계를 자유롭게 할 수 있다는 장점이 있지만 데이터용 클래스와 객체를 위한 클래스를 함께 유지해야 한다는 단점도 존재합니다. 문제의 복잡도와 유지보수 관점에서의 장단점을 기반으로 적합한 방법을 선택하시면 될 것 같아요. 그리고, 책임주도 설계가 이론적으로는 유지보수에 강하다고 하지만, 실제로는 아직 구조가 다소 복잡하게 느껴져서 오히려 유지보수성을 해칠 수도 있지 않을까 합니다. 이런 복잡함은 설계 패턴에 익숙해지면 자연스럽게 해소될까요? 말씀하신 것처럼 객체지향 설계에 대해 아직 익숙하지 않다보니 복잡하고 어렵게 느끼시는게 아닐까 싶어요. 절차적인 설계에서 객체지향 설계로 이동하는 상황을 패러다임 전환(paradigm shift)라고 부를 정도로 절차적인 사고방식을 객체지향적인 사고방식으로 바꾸는 일은 매우 어렵고 힘든 일이기는 합니다(저도 겪었던 문제구요). 절차적인 방식은 새로운 코드를 작성하거나 코드를 읽을 때는 객체지향보다 쉽게 느껴집니다. 반면에 객체지향은 기존 코드에서 수정할 부분을 찾거나 코드를 수정할 때 절차적인 방식보다 더 쉽게 느껴집니다. 다시 말해서 절차적인 방식은 새로운 코드를 작성할 때는 유리하지만 유지보수의 측면에서는 좋지 않고, 객체지향은 새로운 코드를 작성할 때는 불리하지만 유지보수 측면에서는 유리하다고 볼 수 있습니다. 제 생각에 객체지향이 복잡하다고 느끼시는 이유는 처음 코드를 작성할 때 들어가는 노력이 절차적인 방식보다 더 크기 때문이 아닐까 싶어요. 유지보수 단계에서의 장점을 이해하기 위해서는 실제로 변경이 일어날 때 코드를 수정해본 경험이 필요하기 때문에 객체지향에 익숙해지시고 요구사항 변경이 빈번하게 발생하는 상황에서 코드를 수정하는 경험이 쌓이면 유지보수 측면에서 객체지향이 유리한 이유를 이해하시게 될거라고 생각합니다.답변이 되었는지 모르겠네요. 🙂행복한 주말 보내세요!
- 1
- 2
- 54
질문&답변
객체지향 설계에서 메서드를 설계할 때 궁금한 점이 있습니다.
선홍님 안녕하세요.좋은 질문 해주셔서 감사합니다. 🙂제가 아침부터 계속 강의를 하느라 이제서야 답글을 남기네요.답변이 늦어져서 죄송합니다. 객체의 메서드에는 파라미터로 식별자인 id를 전달하는 것보다는 완전한 객체를 전달하는 것이 좋습니다.객체지향에서 객체가 다른 객체를 알아야 하는 이유는 메시지를 전송하기 위해서입니다.이때 객체가 다른 객체를 영구적으로 알아야 한다면 클래스 내부의 객체 참조로 구현하고, 메서드가 실행되는 시점에만 일시적으로 알기만 하면 된다면 메서드의 파라미터로 전달해 주시면 됩니다.이 관점에서 보면 객체의 파라미터로는 객체를 전달하는게 적합하다고 할 수 있습니다. 강의에서 식별자인 id를 파라미터로 받는 클래스이 있는데 ReservationService와 DAO들이 해당됩니다.이 클래스들은 객체지향에서 말하는 상태와 행위를 함께 포함하는 객체가 아니라 예매를 수행하거나 Reservation을 조회하는 등의 행위 관점에서 만들어진 클래스들입니다.이 클래스들은 실제 객체가 아니고 내부에서 id를 이용해서 책임을 수행할 객체를 찾는 작업이 필요하기 때문에 id를 파라미터로 받는다고 보시면 됩니다. 정리하면 현재 메서드를 구현하고 있는 대상이 상태와 행위를 하나의 단위로 묶어서 특정한 책임을 수행하는 '객체'라면 메서드의 파라미터로 객체를 받도록 구현하시는게 좋습니다.그렇지 않다면 파라미터로 id를 받으셔도 무방합니다. 🙂 답변이 되었는지 모르겠네요.행복한 주말 보내세요!
- 2
- 2
- 45




