조영호
@eternity
수강생
2,581
수강평
179
강의 평점
5.0
객체지향 설계와 도메인-주도 설계에 관심이 많으며 행복한 팀과 깔끔한 코드, 존중과 협력이 훌륭한 소프트웨어를 낳는다는 믿음을 가지고 있는 평범한 개발자입니다. 개발자, 교육자, 관리자를 오가며 익힌 다양한 경험을 바탕으로 좋은 코드와 함께 좋은 프로덕트를 만들기 위해 노력하고 있습니다.
저서로는 『객체지향의 사실과 오해』와 『오브젝트』가 있고 번역서로는 『엘레강트 오브젝트』가 있으며 『만들면서 배우는 클린 아키텍처』에 감수자로 참여했습니다.
💡멘토링 신청 : https://inf.run/YvAd2
💡개인블로그 : https://eternity-object.tistory.com/
강의
수강평
게시글
질문&답변
tryMove(..) 메서드 ArrayIndexOutOfBoundsException 제보
이진우님 안녕하세요.개인적인 일이 있어 올려주신 질문을 확인하는게 늦었네요.먼저 죄송하다는 말씀 드립니다. 지적하신 코드는 강의 자료에 오류가 있는 것이 맞고 말씀하신 것처럼 코드를 수정해야만 오류가 발생하지 않습니다.확인해보니 공유드린 예제 코드는 정상적으로 구현되어 있고 강의 자료만 잘못 작성되어 있네요. 강의자료 꼼꼼하게 봐주시고 오류도 제보해주셔서 정말 감사드립니다.우선 공유드린 pdf 강의자료는 수정해서 업데이트해 두었습니다. 보시다가 또 이상한 부분 있으면 질문 남겨주세요. 🙂행복한 하루 보내시구요!
- 1
- 2
- 49
질문&답변
5-4 Sealed Interface는 주로 모든 케이스 검증이 필요할 때 사용하나요?
바나나님 안녕하세요. 좋은 질문 해주셔서 감사합니다. 🙂 질문에서 말씀하신 것과 유사하게 sealed interface는 인터페이스를 구현할 수 있는 종류를 제한하고 싶은 경우에 사용합니다.강의에서 만든 텍스트 어드벤처 예제에서는 사용자가 입력할 수 있는 명령어의 종류를 제한하기 위해 sealed interface를 사용하고 있습니다. 예제 코드를 보시면 Command 타입의 종류로 Move, Unknown, Look, Help, Quit, Inventory, Take, Drop, Destroy, Throw로만 제한한다는 것을 확인할 수 있습니다.public sealed interface Command { record Move(Direction direction) implements Command {} record Unknown() implements Command {} record Look() implements Command {} record Help() implements Command {} record Quit() implements Command {} record Inventory() implements Command {} record Take(String item) implements Command {} record Drop(String item) implements Command {} record Destroy(String item) implements Command {} record Throw(String item) implements Command {} }현재는 인터페이스 안에 record 들을 정의하고 있지만 아래 코드처럼 permits를 사용한 선언문의 형태로 변경하면 종류를 제한하는 sealed interface의 용도를 좀 더 명확하게 이해할 수 있습니다.public sealed interface Command permits Move, Unknown, Look, Help, Quit, Inventory, Take, Drop, Destroy, Throw {} record Move(Direction direction) implements Command {} record Unknown() implements Command {} record Look() implements Command {} record Help() implements Command {} record Quit() implements Command {} record Inventory() implements Command {} record Take(String item) implements Command {} record Drop(String item) implements Command {} record Destroy(String item) implements Command {} record Throw(String item) implements Command {} sealed interface를 사용하면 인터페이스를 구현할 수 있는 모든 타입을 컴파일러가 알 수 있기 때문에 특정 케이스에 대한 처리를 누락하는 문제를 방지할 수 있습니다.Game 클래스의 executeCommand() 메서드를 보시면 switch 문에서 패턴 매칭(pattern matching)을 통해 Command의 종류를 누락하지 않고 저절히 처리하고 있는데 이 경우에 하나의 case문이라도 누락되면 컴파일 에러가 발생하게 됩니다.참고로 default 케이스 없이 모든 구문을 명시적으로 체크할 수 있는 switch 문을 스마트 스위치 표현식(smart switch expression)이라고 부릅니다.private void executeCommand(Command command) { switch(command) { case Command.Move move -> world.tryMove(move.direction()); case Command.Look() -> world.showRoom(); case Command.Help() -> showHelp(); case Command.Quit() -> quit(); case Command.Unknown() -> showUnknownCommand(); case Command.Inventory() -> world.showInventory(); case Command.Take take -> world.takeItem(take.item()); case Command.Drop drop -> world.dropItem(drop.item()); case Command.Destroy destroy -> world.destroyItem(destroy.item()); case Command.Throw aThrow -> world.throwItem(aThrow.item()); } }위 코드를 아래의 Move, Take, Drop, Destory, Throw에서 알 수 있는 것처럼 Command.Move(Direction direction)의 형태로 내부 요소를 분해해서 처리할 수도 있습니다.private void executeCommand(Command command) { switch(command) { case Command.Move(Direction direction) -> world.tryMove(direction); case Command.Look() -> world.showRoom(); case Command.Help() -> showHelp(); case Command.Quit() -> quit(); case Command.Unknown() -> showUnknownCommand(); case Command.Inventory() -> world.showInventory(); case Command.Take(String item) -> world.takeItem(item); case Command.Drop(String item) -> world.dropItem(item); case Command.Destroy(String item) -> world.destroyItem(item); case Command.Throw(String item)-> world.throwItem(item); } } 참고로 sealed interface와 record는 Java에서 대수적 자료형(Algebraric Data Type)을 구현하기 위해 사용됩니다.대수적 자료형은 합 타입과 곱 타입을 조합해서 구현하며 자바의 경우 sealed interface는 OR로 조합할 수 있는 합 타입(Sum Type)을, record를 이용해서 다양한 상태를 조합할 수 있는 곱 타입(Product Type)을 사용해서 구현합니다. 강의에서는 특별히 언급하지 않았지만 데이터와 로직을 하나의 단위로 묶는 객체지향과 달리, Command의 경우처럼 특정한 규칙에 따라 데이터와 로직을 나누는 패러다임을 데이터 지향 프로그래밍(DOP, Data-Oriented Programming)이라고 부릅니다.자바에서 데이터 지향 프로그래밍은 sealed interface와 불변 데이터를 구현하기 위한 record, 별도의 모듈에서 행동을 구현하기 위한 패턴 매칭을 조합해서 구현합니다.데이터는 sealed interface와 record를 이용해서 불변 데이터로 구현하며, 이 로직을 사용하는 로직은 executeCommand() 메서드와 같이 패턴 매칭을 이용해서 타입에 따라 적절한 처리를 수행합니다.객체지향이 객체의 행동에 초점을 맞추고 상태화 행동을 함께 묶어 행동을 통해 상태 변경을 캡슐화하는 방식이라면, 데이터 지향 프로그래밍은 데이터에 초점을 맞추는 대신 상태 변경으로 인한 부수효과를 억제하기 위해 불변 데이터를 사용하고 패턴 매칭을 통해 별도의 모듈에서 행동을 구현하는 방식이라고 보시면 될것 같아요. 정리하면 예제 코드는 복잡한 게임 플레이 로직은 객체지향 프로그래밍 방식을 따르고, 명령 처리 로직은 데이터 지향 프로그래밍 방식을 따르고 있습니다.강의의 예제처럼 애플리케이션을 구현할 때는 하나의 패러다임만 적용하는게 아니라 여러 종류의 패러다임을 조합해서 애플리케이션을 구축하는 방식이 일반적입니다(따라서 자바는 멀티패러다임 언어라고 할 수 있습니다). 답변이 되었는지 모르겠네요. 😊행복한 성탄절 보내시고 추가로 궁금한 부분이 있으면 질문 남겨주세요.
- 1
- 1
- 432
질문&답변
이 강의만 자료가 ppt네요.
ansxjrdptj94님 안녕하세요.캡슐화에 ppt 파일이 올라가 있었네요.파일을 수정해서 올릴 때 잘못 올린것 같아요.확인해 주셔서 감사합니다 🙂행복한 하루 보내세요.
- 1
- 1
- 87
질문&답변
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
- 94
질문&답변
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
- 54
질문&답변
강의 자료 관련 질문입니다! (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
- 81
질문&답변
도메인 관련 질문이 있습니다!
테디베어님 안녕하세요.죄송하지만 질문 내용을 제가 정확하게 이해할 수 없어서 정확한 답변을 드리기 위해 몇 가지 질문을 드려야 할 것 같습니다. Q1. "영화 관람료"라는 객체를 추가하고 상영 객체가 의존한다고 하셨는데 구체적으로 영화 관람료가 맡은 책임은 무엇이고, 상영이 영화 관람료에 어떤 방식으로 의존(또는 협력)하는 것일까요? 이 부분을 이해해야만 답변을 드릴 수 있는데 의사 코드 수준으로라도 적어주시면 이해하는데 도움이 될 것 같아요. Q2. "영화 관람료"를 추가해서 상영이 할인 정책과 협력하면 지금 방식보다 어떤 점이 개선된다고 생각하시나요? 이 방식이 현실과 더 밀접하다고 하셨는데 어떤 점에서 그런지도 적어 주시면 좋겠습니다. 위 내용을 적어주시면 적어주신 내용 기반으로 정확한 답변을 드릴 수 있을것 같아요! 🙂
- 1
- 2
- 101
질문&답변
6-2 보호 로직 중복 이슈
mint.inhrdev님 안녕하세요. 다음 질문과 중복되어 답변 대신 링크로 대신합니다.https://inf.run/41yfx 답변 내용을 보시면 궁금하신 부분에 대해 명확하게 이해하실 수 있으실거에요. 🙂 질문 남겨 주셔서 감사하고 해당 답변을 읽으신 후에도 궁금한 부분이 있으면 추가로 질문 남겨주세요. 감사합니다!
- 1
- 2
- 92
질문&답변
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
- 116
질문&답변
영화, 상영, 예매 도메인 관계에 대한 질문
동관님 안녕하세요.좋은 질문 남겨 주셔서 감사합니다. 🙂먼저 영화와 예매는 N:M 관계가 아니라 1:N 관계입니다.어떤 관계에서 다중성(multiplicity)은 하나의 객체에 대해 연결될 수 있는 다른 객체의 개수를 나타냅니다.하나의 영화에는 여러 개의 예매가 만드어질 수 있는데 반해 예매는 하나의 영화에 대해서만 생성될 수 있기 때문에 영화:예매는 1:N 관계에 해당됩니다.상영은 그 자체로 도메인에 존재하는 중요한 개념입니다.아래 그림에서 영화는 "어쩔수가없다"이고 아래에 예매 가능한 4개의 박스가 상영에 해당됩니다.(사진) 참고로 다대다 관계를 해소하기 위해 중간 테이블을 추가하는 개념은 데이터베이스 모델링의 영역에 한정되는 기법입니다.객체 관계에서는 다대다 관계가 가능하기 때문에 다대다 관계를 해소하기 위해 중간 객체를 추가할 필요가 없고 협력 관점에서 특정한 동작을 수행할 객체가 필요하거나 도메인 관점에서 명시적으로 드러내야 하는 경우에 적절한 객체를 추가하게 됩니다. 답변이 되었는지 모르겠네요. 🙂감사합니다.
- 1
- 2
- 77




