객체지향 설계와 도메인-주도 설계에 관심이 많으며 행복한 팀과 깔끔한 코드, 존중과 협력이 훌륭한 소프트웨어를 낳는다는 믿음을 가지고 있는 평범한 개발자입니다. 개발자, 교육자, 관리자를 오가며 익힌 다양한 경험을 바탕으로 좋은 코드와 함께 좋은 프로덕트를 만들기 위해 노력하고 있습니다.
저서로는 『객체지향의 사실과 오해』와 『오브젝트』가 있고 번역서로는 『엘레강트 오브젝트』가 있으며 『만들면서 배우는 클린 아키텍처』에 감수자로 참여했습니다.
💡개인블로그 : https://eternity-object.tistory.com/
講義
受講レビュー
- オブジェクト - 基礎
投稿
Q&A
간접 참조에 대한 질문
박명규님 안녕하세요.좋은 질문 남겨주셔서 감사합니다. 간접참조를 사용하는 방식은 도메인 주도 설계(DDD)에서 애그리게이트(Aggregate) 사이의 직접적인 객체 참조 대신 서로의 식별자만 참조해서 결합도를 낮추는 방법입니다.질문의 요지가 “간접참조를 사용할 경우의 객체지향적인 설계 방법”을 문의하셨는데 사실 간접 참조를 사용하는 방식은 객체지향 설계의 일부 제약을 완화하게 됩니다.따라서 객체지향의 설계에 객체 참조를 사용할 경우에는 객체지향 원칙과는 조금 다르게 DDD의 규칙에 따라 설계를 진행하게 됩니다.질문으로 돌아와서 Screenig에 movieId를 참조할 경우 몇가지 해결 방법이 있습니다. 1. Movie 인스턴스 전달Screening에 Reservation을 생성하는 책임을 그대로 둘 경우 아래 처럼 movieId에 해당하는 Movie를 전달할 수 있습니다.이 Movie는 질문에서 언급하신 것처럼 서비스 레이어에서 DAO를 이용해서 조회한 후에 전달하면 됩니다.public class Screening { private Long id; private Long movieId; private Integer sequence; private LocalDateTime screeningTime; public class Screening { public Reservation reserve(Long customerId, Movie movie, int audienceCount) { if (!movieId.equals(movie.getId()) { throw new IllegalArgumentException(); } Money fee = movie.calculateFee(toScreeningCondition()).times(audienceCount); return new Reservation(customerId, this.id, audienceCount, fee); } public ScreeningCondition toScreeningCondition() { return new ScreeningCondition(sequence, screeningTime); } ... }이 방식은 주의할 부분이 있는데 DDD에서는 단일 트랜잭션 안에서 하나의 애그리게이트만 수정해야 하기 때문에 Movie의 상태를 변경해서는 안됩니다.따라서 Movie의 메서드 중에서 부수 효과가 없는 메서드만 사용해야 하는데 메서드를 주의해서 사용하거나 CQS(명령-쿼리 분리) 원칙에 따라 구성할 필요가 있습니다.여기에서는 Screening이 Reservation에도 결합되게 되는데 이 의존성이 문제가 된다면 이어지는 2번처럼 생성 책임을 외부로 뺄 수 있습니다. 2. Screening과 Movie의 의존성은 낮추고 Movie에게 값 객체 전달1번의 이슈가 문제가 되는 경우에는 Screening과 Movie를 직접 의존하게 만들지 않고 둘 사이의 협력을 애플리케이션 서비스인 ReservationService에서 조율하는 방법을 사용합니다.이 경우 결합도를 낮추기 위해 Movie에서 사용하는 Screenin의 데이터는 값 객체로 변환해서 Movie의 calculateFee 메서드에 전달합니다.@Service public class ReservationService { @Transactional public Reservation reserveScreening(Long customerId, Long screeningId, Integer audienceCount) { Customer customer = customerRepository.findById(customerId).get(); Screening screening = screeningRepository.findById(screeningId).get(); Movie movie = movieRepository.findAggregateById(screening.getId()).get(); Money fee = movie.calculateFee(screening.toScreeningCondition()); Reservation reservation = new Reservation(customer.getId(), screening.getId(), audienceCount, fee); reservationRepository.save(reservation); return reservation; } } 지금까지 설명드린 것처럼 DDD의 애그리게이트는 일관성 경계를 위해 객체 사이의 협력을 제한하는 경향이 있기 때문에 간접참조를 활용하는 방법은 객체지향의 설계 원칙을 트레이드오프하게 됩니다.개인적으로는 부수효과를 잘 통제한다면 1번처럼 파라미터로 전달하는 방식도 가능하다고 보고 있습니다.하지만 2번 방식처럼 서로 참조하지 않도록 만들 수 있다면 이 방식을 따르는게 더 좋은 방법이겠죠.여기에서 설명드린 부분에 대한 자세한 내용이 궁금하시다면 도메인 주도 설계의 전술적 설계 부분을 깊이 있게 학습하시는 것을 추천드립니다. 관련해서 궁금한 부분에 대해 추가로 질문해 주시면 좀 더 자세히 설명드릴게요. 😊감사합니다.
- 1
- 2
- 43
Q&A
인스턴스 증가에 대한 우려
hello님 안녕하세요.강의 들어주시고 좋은 질문도 남겨 주셔서 감사합니다. 🙂두 질문에 대해 차례대로 답변 드리도록 할게요! 1.ObjectMapper를 싱글턴으로 만들지 여부강의에서 사용한 예제 코드의 경우에는 단일 책임 원칙에 초점을 맞추고 예제의 구현을 단순화하기 위해 메서드 내부에서 ObjectMapper를 생성하고 있지만, 다수의 요청을 동시에 처리해야 하는 실무 코드에서는 ObjectMapper를 생성한 후에 필요한 곳에서 의존성 주입을 받아 사용하시는게 좋습니다.사이즈가 작고 단순한 객체라면 메서드 내부에서 지역 객체로 생성하거나 인스턴스 변수로 선언하고 매번 생성해도 무방하지만 ObjectMapper처럼 객체 초기화 비용이 큰 경우에는 하나의 인스턴스를 공유해서 사용하는게 효과적입니다.이렇게 여러 객체가 하나의 인스턴스를 공유해서 사용하기 위해서는 이 인스턴스가 쓰레드에 안전(thread-safe)해야 하는데, 다행히도 ObjectMapper의 경우에는 쓰레드에 안전하게 설계되어 있습니다.따라서 여러 인스턴스가 ObjectMapper를 공유해서 쓰더라도 문제가 발생하지 않습니다.다만 ObjectMapper의 상태를 설정하는 setter 계열 메서드는 쓰레드에 안전하지 않기 때문에 애플리케이션이 구동되는 시점에 필요한 상태를 가지도록 ObjectMapper를 초기화하고, 애플리케이션이 실행되는 동안에는 ObjectMapper의 상태를 바꾸지 말아야 합니다.Spring을 사용할 경우에는 애플리케이션 구동 시점에 아래 코드처럼 필요한 상태를 가지는 ObjectMapper 싱글톤을 생성해서 애플리케이션 컨텍스트에 등록한 후 필요한 곳에서 의존성 주입해서 사용하는 방식으로 구현합니다(Boot의 경우에는 기본 ObjectMapper가 미리 등록되어 있어 빈 등록 과정 없이 의존성 주입 받아 사용할 수 있습니다).@Configuration public class ObjectMapperConfig { @Bean // ObjectMapper 객체 빈 등록 , 빈의 이름은 메소드 이름을 따라간다 public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.registerModule(new JavaTimeModule()); // 필요한 설정 추가 ... return mapper; } } 2.인스턴스 변수와 메서드 파라미터의 차이첫 번째 차이점은 객체 인스턴스의 의미가 다르다는 점입니다.질문에 적어주신 코드를 예로 들어서 어떤 의미적인 차이가 있는지 설명해 드릴게요.먼저 dayOfWeek을 파라미터로 받는 WeeklyMenu는 “모든 요일의 메뉴 정보를 다 관리하는 책임”을 맡고 있는 객체를 의미합니다.따라서 이 객체는 어떤 요일의 메뉴를 물어보더라도 다 대답을 할 수 있죠.public class WeeklyMenu { public String getMenu(DayOfWeek dayOfWeek) { return switch (dayOfWeek) { case MONDAY -> "짜장면"; case TUESDAY -> "짬뽕"; case WEDNESDAY -> "제육덮밥"; case THURSDAY -> "볶음밥"; case FRIDAY -> "간짜장"; case SATURDAY -> "미니탕수육"; case SUNDAY -> "잡채밥"; }; } } 반면에 dayOfWeek을 인스턴스 변수로 가지는 WeeklyMenu는 “특정한 하나의 요일의 메뉴 정보만 관리하는 책임”을 맡고 있는 객체를 의미합니다.public class WeeklyMenu { private final DayOfWeek dayOfWeek; public WeeklyMenu(DayOfWeek dayOfWeek) { this.dayOfWeek = dayOfWeek; } public String getMenu() { return switch (this.dayOfWeek) { case MONDAY -> "짜장면"; case TUESDAY -> "짬뽕"; case WEDNESDAY -> "제육덮밥"; case THURSDAY -> "볶음밥"; case FRIDAY -> "간짜장"; case SATURDAY -> "미니탕수육"; case SUNDAY -> "잡채밥"; }; } } 만약 이 객체를 다음과 같이 생성했다면 이 객체는 월요일의 메뉴에 대해서만 대답할 수 있습니다.WeeklyMenu menu = new WeeklyMenu(DayOfWeek.MONDAY); 다시 말해서 파라미터로 받는 WeeklyMenu는 “모든 요일의 메뉴”를 의미하고, 인스턴스 변수로 가지는 WeeklyMenu는 “특정한 요일의 메뉴”를 의미합니다.이 의미적인 차이는 코드의 의도를 전달할 때 매우 큰 차이를 보입니다.두 번째 차이점은 방금 전에 설명드린 의미적인 차이점으로부터 파생되는 내용인데, 파라미터로 받는 WeeklyMenu는 어떤 요일의 메뉴 정보가 필요한지를 클라이언트가 판단하고 결정합니다.즉, 요일을 관리할 책임이 클라이언트에게 있고, WeeklyMenu는 클라이언트가 원하는 요일의 메뉴를 전달해주기만 하면 됩니다.반면에 인스턴스로 가지는 WeeklyMenu의 경우에는 요일을 관리할 책임이 WeeklyMenu에게 있습니다.클라이언트는 어떤 요일인지 모르는 상태에서 WeeklyMenu가 반환해주는 메뉴를 받기만 합니다.따라서 변화하는 값(이 경우에는 요일)을 클라이언트가 알고 요청해야 하는지, 아니면 그 값이 객체의 본질적인 의미여서 클라이언트는 단지 객체가 알아서 판단해주기를 바라는지에 따라인스턴스 변수를 사용할지, 파라미터를 사용할지를 결정하게 됩니다. 답변이 되었는지 모르겠네요. 😊
- 1
- 2
- 46
Q&A
6-2. 명령과 쿼리 분리 원칙 질문
강명덕님 안녕하세요.두 가지 질문에 대해 차례대로 답변을 드릴게요. 1. Game에서 if 분기를 사용한 이유강의의 흐름을 보시면 Player 클래스의 move 메서드가 CQS를 위반하는 move 메서드를 제공한다는 것을 알 수 있습니다.move는 이동 여부를 반환하는 쿼리인 동시에 position을 변경하는 명령이었죠.public class Player { public boolean move(Direction direction) { if (worldMap.isBlocked(position.shift(direction))) { return false; } this.position = this.position.shift(direction); return true; } 이 메서드를 CQS를 만족하도록 만들려면 move 메서드의 반환 타입은 void로 변경해야 합니다.하지만 이렇게 하면 클라이언트인 Game 클래스가 성공 여부를 알 수 없겠죠.여기에서 잠깐 객체지향에서 예외(exception)의 올바른 사용 방법을 살펴볼 필요가 있는데 예외는 정상적인 플로우가 아니라 발생할 수 없는 예외적인 케이스에만 사용하는게 좋습니다.(실무에서는 실용적인 이유때문에 플로우 변경을 위해 예외를 사용하는 경우가 많지만 엄격하게 보면 올바른 예외 사용 방법은 아니라고 생각하시는게 좋아요).여기에서 캐릭터가 지도를 이동하던 도중에 벽이 막혀서 이동할 수 없다는 플로우는 예외적인 케이스가 아니라 정상적인 플로우에 속합니다.따라서 예외를 던지는게 아니라 canMove 같은 쿼리를 이용해서 클라이언트가 이동 가능한 지 체크해서 막히자 않은 경우에만 이동한다는 플로우를 명시적으로 표시해 주는게 좋겠죠.public class Game { private void tryMove(Direction direction) { if (player.canMove(direction)) { // 이동 가능한 경우에는 player.move(direction); // 지도 이동 showRoom(); return; } showBlocked(); // 막혀있다면 이동 불가능 } } 그러면 Player의 move 메서드에서 다시 canMove 메서드를 호출한 뒤에 예외를 던지는 이유가 궁금하실텐데요이 상황에서 예외가 발생한다는 것은 클라이언트가 반드시 canMove 메서드를 호출해서 이동 가능한지 여부를 확인한 후에 move 메서드를 호출해야 한다는 계약을 어긴 경우이기 때문입니다.따라서 정상적인 상황이 아니라 인터페이스를 잘못 사용한 예외적인 경우이기 때문에 예외를 던지는게 정당화됩니다. 2. TDA 위반 여부1번에 대한 답변에서 if문을 사용한 이유는 이해가 되셨을것 같고 대신 TDA에 대해 좀 더 자세히 설명드릴게요.Player의 move 메서드를 보시면 canMove 메서드를 호출해서 이동 가능한지 스스로 체크하고 이동 불가능한 경우 예외를 던지기 때문에 자신의 상태를 스스로 관리하고 있습니다.따라서 Player의 move 메서드는 스스로 판단하고 스스로 자신의 위치를 변경하기 때문에 자율적인 객체이며 TDA 위반이 아닙니다.TDA를 위반하는 코드는 아래 처럼 필요한 상태를 물어본 후에 반환 값을 이용해서 직접 객체의 상태를 변경하는 구조를 가집니다.public class Game { private void tryMove(Direction direction) { Position position = player.position(); WorldMap worldMap = player.worldMap(); if (!worldMap.isBlocked(position.shift(direction))) { player.setPosition(position.shift(direction)); showRoom(); return; } showBlocked(); } } 위 코드를 보시면 Player에게 내부에 포함하고 있는 position과 worldMap에 대해 물어본 후에 이들을 기반으로 결정하고 position을 직접 수정하고 있습니다.반면에 강의의 코드는 내부 상태가 아니라 이동 가능한지 여부를 물어보고(이 판단은 Player가 직접 합니다) 어떤 위치로 가야하는지도 Player가 직접 결정합니다.move 메서드 안의 if문을 제거하더라도 클라이언트가 내부의 상태를 물어본 후에 직접 가능 여부를 판단하지 않고 Player가 스스로 판단하기때문에 TDA 위반이 아닙니다.따라서 앞에서 설명드린 것처럼 if문은 Player 와 협력하는 다른 클라이언트가 canMove 호출 없이 move를 호출하는 인터페이스 사용 규칙을 위반한 경우를 대비해서 넣은게 맞습니다. 답변이 되었는지 모르겠네요. 😊
- 1
- 2
- 349
Q&A
9-1 사소한 강의자료 오류
강명덕님 안녕하세요.확인해 보니 X의 위치가 target이 아닌 source로 이동해야 하는군요.동영상 자료 수정에 조금 시간이 걸려서 최대한 빠르게 수정 후에 말씀드릴게요. 🙂자세히 봐주시고 제보해 주셔서 감사합니다!
- 1
- 2
- 39
Q&A
7-3 상속을 이용한 중복 제거 질문 있습니다!
강명덕님 좋은 질문 남겨 주셔서 감사합니다!두 질문에 대해 차례대로 답변드릴게요. 🙂 1. 협력을 위해 제공하는 메시지를 확인하기 용이하기 때문인가요?먼저 첫 번째 질문인 “추상 클래스로만 추상화를 한 뒤, 상위 수준 객체가 인터페이스 대신 추상 클래스를 의존하면 안될까?”에 대해 답변 드릴게요.강의에서 설명드렸던 것처럼 JsonReader와 CsvReader에서 추상화를 추출하는 이유는 클라이언트인 CallCollector 입장에서 두 객체가 교체 가능해야 하기 때문입니다.이를 위해서는 CallCollector가 구체적인 JsonReader나 CsvReader에 의존하는 대신 두 타입의 객체를 포괄하는 상위 타입에만 의존하도록 의존성을 조정해야 합니다.여기에서 고민은 말씀하신 것처럼 Reader를 인터페이스 대신 추상 클래스로 만들 것인지, 아니면 인터페이스로 만들고 중복 코드는 추상 클래스인 AbstractReader에 남기는 안을 선택할 지를 결정해여 한다는 것입니다. 강의와 다르게 Reader를 추상 클래스로 선언하면 말씀하신 것처럼 read만 public이기 때문에public abstract class Reader { private String path; public Reader(String path) { this.path = path; } public List read() { List lines = readLines(path); return parse(lines); } private List readLines(String path) { try { return Files.readAllLines( Path.of(ClassLoader.getSystemResource(path).toURI())); } catch (Exception e) { throw new RuntimeException(e); } } protected abstract List parse(List lines); } CallCollector는 추상 클래스인 Reader를 의존성 주입받아서 사용하면 되겠죠.public class CallCollector { private final Reader reader; public CallCollector(Reader reader) { this.reader = reader; } public CallHistory collect(String phone) { List calls = reader.read(); ... } } 이렇게 구현을 해도 CsvReader와 JsonReader를 교체하겠다는 목표는 달성할 수 있습니다.여기에서 7-3을 포함하는 섹션7의 타이틀을 “외부 의존성과 테스트”라고 지었다는 점에 주목해 주시면 좋겠습니다.지금 우리는 CallCollector를 좀 더 쉽게 테스트하고 싶다는 점도 목표로 하고 있습니다.테스트 관점에서 현재의 Reader에서 신경 쓰이는 부분은 Reader가 외부의 파일 시스템에 의존하고 있다는 점입니다.Reader의 내부 구현은 런타임에 파일 시스템이 존재한다는 것을 가정하고 있습니다.단위 테스트는 외부의 파일 시스템에 의존하지 않고 메모리 안에 생성된 객체들을 이용해서 실행하는 것이 이상적이기 때문에 파일에 대한 의존성을 제거하고 테스트할 수 있다면 좋겠죠.Reader를 인터페이스로 만들면 파일에 대한 의존성을 제거할 수 있습니다.public interface Reader { List read(); } 파일에 대한 의존성은 AbstractReader로 이동시켰습니다.public abstract class AbstractReader implements Reader { private String path; public Reader(String path) { this.path = path; } public List read() { List lines = readLines(path); return parse(lines); } private List readLines(String path) { try { return Files.readAllLines( Path.of(ClassLoader.getSystemResource(path).toURI())); } catch (Exception e) { throw new RuntimeException(e); } } protected abstract List parse(List lines); } 이제 CallCollector는 파일에 대한 의존성을 가지지 않는 Reader 인터페이스에만 의존하기 때문에 쉽게 외부 의존성을 고려하지 않고도 쉽게 테스트할 수 있습니다.실제로 7-3의 11:07 부분을 보시면 가짜 객체(Fake Object) 방식으로 구현된 테스트 더블인 FakeReader를 이용해서 단위 테스트를 쉽게 실행할 수 있는 방법을 보여주고 있습니다.public class FakeReader implements Reader { private List calls; public FakeReader(Call ... calls) { this.calls = List.of(calls); } @Override public List read() { return calls; } } 물론 Reader가 추상 클래스인 경우에도 가짜 객체를 추가할 수는 있습니다.하지만 추상 클래스의 경우에는 Reader의 내부 구현을 이해한 상태에서 가짜 객체를 구현해야 하기 때문에 복잡해집니다.다음은 Reader가 추상 클래스인 경우의 FakeReader 구현입니다.public class FakeReader extends Reader { private List calls; public FakeReader(Call ... calls) { super(""); this.calls = List.of(calls); } @Override public List read() { return calls; } @Override protected List parse(List lines) { return List.of(); } } 상속은 내부 구현에 대한 결합도가 높기 때문에 내부 구현을 이해해야 한다는 점이 항상 문제를 일으킵니다.FakeReader를 구현하기 위해서는 Reader의 내부 구현을 이해해야 합니다.read 메서드만 오버라이딩하면 파일에 대한 의존성을 제거할 수 있다는 사실을 이해해야 합니다.FakeReader의 생성자에서 super 콜을 이용해서 Reader의 생성자를 호출해야 한다는 사실을 알고 있어야 합니다.Reader의 내부 구현에 속하지만 테스트와는 상관이 없는 parse 메서드도 오버로딩해야 합니다.이 경우에는 테스트를 위해 내부 구현에 대한 결합을 완전히 끊어내기 위해 인터페이스를 사용하는게 더 효과적입니다. 말씀하신 것처럼 인터페이스가 추상 클래스보다 역할을 한 눈에 파악하기 쉬운건 맞지만 설계 원칙을 기반으로 추상 클래스를 작게 만들 경우에는 이 부분에서 생각보다 차이가 크지는 않은 것 같아요.인터페이스를 하나 추가할 때의 비용(머리 속에서 트래킹해야 하는 요소가 하나 추사된 거니까요) 대비 이익이 크지 않다면 추상 클래스로 시작하시는게 좋습니다.이런 이슈가 발생하지 않고 단위 테스트 관점에서 큰 차이가 없는 경우라면 저도 강명덕님의 생각과 동일하게 추상 클래스로 시작했을 겁니다.나중에 여러 클래스 계층에 적용할 필요가 있는 경우에 인터페이스를 추출하는건 생각보다 쉽기 때문이죠. 2. 두 public 메서드 중 하나는 중복 로직, 하나는 각 구현체마다 다르게 구현하는 경우에도 인터페이스와 추상클래스를 함께 사용하는 것이 좋을까요?모든 자식 클래스들이 단일 상속 계층 안에 위치하는지 여부와 단위 테스트에 대한 필요성에 따라 판단하시면 될것 같습니다.만약 자식 클래스들이 여러 상속 계층을 구성한다면 인터페이스로 만드시는게 좋습니다.동일한 상속 계층에만 속한다면 인터페이스와 추상 클래스 둘 중 하나를 선택할 수 있습니다.CaptchaHashProcessor와 협력하는 다른 클래스를 단위 테스트해야 한다면 verify 메서드의 내부 구현이 통제하기 어려운 외부 요소에 의존하고 있는지 살펴보시는게 좋습니다.만약 verify 메서드가 불안정한 외부 요소를 사용한다면 인터페이스로 만드시는게 모킹이나 가짜 객체를 만들기에 용이합니다.만약 verify 메서드가 통제 가능한 요소들만 사용한다면 추상 클래스로 만드셔도 무방합니다. 이렇게 모든 클래스가 단일 상속 계층에 속하고 불안정한 요소게 의존하지 않기 때문에 단위 테스트에 문제를 일으키지 않는다면 말씀하신 것처럼 일단 추상 클래스에 의존하도록 만드셔도 무방합니다.대신에 추상 클래스의 이름은 Abstract를 제외한 CaptchaHashProcessor로 지으시는게 좋습니다.이렇게 하면 나중에 CaptchaHashProcessor를 인터페이스로 변경하더라도 클라이언트에 주는 영향을 최소화할 수 있습니다. 중복을 제거하고 코드를 더 쉽게 재사용할 수 있는 더 좋은 방법으로는 합성(Composition)이 있으며 색션 8에서 관련된 내용을 다루고 있습니다. 질문의 내용 구성에 대해 의견을 요청하셨는데 궁금하신 내용을 명확하게 정리해 주셔서 답변을 드리기가 쉬웠습니다.질문도 흥미로운 부분이라 강의에서 해당 부분을 좀더 자세히 설명했으면 좋았었겠구나라는 생각이 들었습니다.좋은 질문해 주셔서 감사드리고 답변 중에 미진한 부분이나 추가로 궁금한 부분 있으면 질문 남겨주시면 감사하겠습니다!
- 3
- 1
- 1K
Q&A
함수형 스타일에 대해선 어떻게 생각하시나요?
빡소리님 안녕하세요. 저같은 경우에는 객체지향쪽에 치우쳐 있기 때문에 객체지향 패러다임을 중심으로 함수형 패러다임을 접목시키는 방식으로 사용하고 있습니다. 전체 구조를 메시지를 주고 받는 객체사이의 협력을 기반으로 구현한다는 점은 동일합니다.차이점은 객체의 상태 변경 대신 새로운 객체를 반환하거나 도메인 이벤트를 발행시키는 등 함수형을 제한적으로 사용하고 있습니다.객체 인터페이스에 나타나는 람다는 strategy 패턴을 대체하는 정도의 용도로만 제한적으로 사용하고 있습니다. 코틀린 진영을 보면 불변 data 클래스를 사용하고 로직을 함수로 분리하는 방식으로 좀 더 적극적으로 함수형 프로그래밍을 적용하시는 케이스에 대한 이야기를 많이 듣고 있는데 이 부분에 대해서는 저도 경험이 많지 않아서 깊이 있는 내용을 말씀드리기 어렵네요. 🙂 설명이 부족해서 죄송합니다.
- 1
- 2
- 70
Q&A
값 객체 활용에 대해
빡소리님 안녕하세요. 좋은 질문해 주셔서 감사합니다. 당연한 이야기겠지만 값 객체 컬렉션이 필요한 경우에는 @ElementCollection을 지정해서 값 객체를 별도의 테이블에 저장합니다.하지만 하나의 값 객체만 필요한 경우에는 동일한 테이블에 저장해서 영속성 전이나 생명 주기 관리 등의 복잡성을 JPA가 처리하도록 만드는게 좋습니다. 값 객체를 별도의 테이블에 저장하려면 클래스에 @Embeddable이 아닌 @Entity를 붙이고 영속성 전이와 생명 주기 관리와 관련된 애너테이션을 직접 지정해주면 됩니다.대표적인 케이스로는 값 객체에 상속을 적용해서 다형적인 행위를 구현할 필요가 있거나, 값 객체 컬렉션에 요소를 추가할 때 실행되는 쿼리를 줄이고 싶을 경우 등에 한해서 클래스에 @Entity를 붙이고 값 객체로 구현하기도 합니다.다만 이 경우에는 영속성 전이와 생명 주기 관리와 관련된 부분을 개발자가 직접 지정해줘야하고, @Entity와 값 객체 사이의 불일치를 기억해야 하는 부담이 따를뿐만 아니라, 추가적인 테이블 조인이 발생할 수 있기 때문에 꼭 필요한 경우로 한정해서 사용하는게 좋습니다. 답변이 되었는지 모르겠네요.
- 1
- 2
- 96
Q&A
실례지만 여기에 후기 올립니다.
ycseol님께 드렸던 답변 이후에도 이 주제가 계속 머릿속을 맴돌아 며칠 뒤 따로 생각을 정리해서 링크드인에 올렸습니다.혹시 관심 있으실까 싶어 여기에도 함께 공유드립니다.질문을 통해 강의 방향에 대해 다시 고민해보는 계기가 되었습니다.좋은 인사이트를 주신 점 다시 한번 감사드립니다.인프런의 오브젝트 - 설계 원칙편을 수강하신 분이 감사하게도 Q&A에 정성스러운 후기를 남겨주셨습니다. 수강생분의 의견을 한 줄로 요약하면 자바 진영의 표준으로 사용되는 스프링을 기반으로 설계 원칙을 적용한 예제도 있었으면 좋지 않겠냐로 요약할 수 있습니다. 이 후기를 읽고나서 한 동안 머릿속으로 많은 생각이 흘러 지나갔습니다. 그리고 여러 번 답변을 수정하는 동안 제가 가지고 있던 생각에 대해서도 다시 한번 돌아보는 계기가 되기도 했습니다.사실 이 부분은 첫 번째 강의인 오브젝트 - 기초편을 만들기 시작할 때부터 많이 고민했던 주제이기도 합니다. 그리고 그 때의 고민이 이번 후기를 통해 강의의 정체성과 방향성에 대한 새로운 고민으로 이어지게 되었구요. 아무래도 국내의 개발자 대부분이 스프링을 주요 프레임워크로 사용하고 있고 많은 인기 강의들이 스프링(그리고 JPA) 중심으로 구성되어 있다보니 강의를 만드는 입장에서는 스프링 기반의 강의를 만드는게 ROI 관점에서 훨씬 더 효율적으로 다가올 수 밖에 없습니다. 그럼에도 스프링을 포함한 특정 프레임워크나 기술을 최대한 배제하고 강의를 만든 이유는 오롯이 객체지향적인 사고방식과 설계에 집중하는 강의로 시작해서 스프링이나 JPA를 사용한 강의로 나아가는 로드맵을 계획하고 있기 때문입니다. 스프링 안에 객체지향 설계를 가둬두기 보다는 제가 성장했던 경험을 기반으로 객체지향에 대한 이해도를 높여서 자연스럽게 스프링을 포함한 다양한 코드를 이해할 수 있도록 만드는게 좋지 않을까라는 생각도 있었구요.하지만 전체는 아니더라도 일부 예제라도 스프링을 기반으로 설명해주면 좋지 않겠냐는 의견은 제 입장에서 깊이 고민해봐야하는 주제라고 생각합니다. 다음 강의는 원래의 계획대로 기본 개념을 쌓는 쪽으로 갈 지, 아니면 기존 강의에 라이브 코딩을 보강하거나 실무 관점에서 지금까지 설명했던 개념들을 적용하는 중간 단계의 실무 강의를 만드는게 좋을 지 좀 더 고민해 봐야겠어요. 저자가 전달하고 싶은 내용을 담게 되는 책과 다르게 빠르게 피드백을 주고 받으면서 경로를 수정할 수 있는게 온라인 강의의 매력이라는 생각이 드네요. 후기를 남겨주신 분처럼 강의에 대한 다양한 의견을 주시면 기존 강의를 개선하거나 이후의 방향을 조정하는데 큰 도움이 될 것 같습니다. 🙂
- 2
- 3
- 427
Q&A
실례지만 여기에 후기 올립니다.
ycseol님 안녕하세요.먼저 강의에 대한 의견 주셔서 감사합니다. :)개인적으로도 언젠가는 스프링이나 JPA 관련된 강의를 하는게 좋지 않을까라는 생각을 하고 있는데 일단 객체지향 - 설계 원칙편 강의를 만드는 동안에는 순수하게 객체지향 자체에 집중하는 쪽을 선택했어요.이유를 간단히 말씀드리겠습니다. “시중에 나와 있는 서적과 강좌가 정말 잘 되어 있는 만큼 사람들이 많이 사용하는 스프링 프레임워크에 적용한 예제도 가끔 있으면 어떨까 합니다”라고 말씀해 주셨는데 오브젝트 - 설계 원칙편은 의도적으로 스프링이나 JPA와 같은 특정 기술과 관련된 내용을 배제하고 객체지향적인 코드를 작성할 때 필요한 원칙과 사고방식에 집중할 수 있도록 구성했습니다.몇 가지 예제를 제외하면 가급적 Java 표준에 포함된 라이브러리를 사용한 이유도 그 때문이구요.이 부분은 개인적으로 고민을 많이 한 부분인데요 스프링이나 JPA처럼 많이 사용되는 기술을 기준으로 강의를 구성하는 편이 강의 판매 측면에서는 더 좋은건 당연한 이야기이기 때문입니다. 하지만 이 경우에는 객체지향 설계를 할 때 어떻게 사고하면 좋은지에 초점을 맞추는게 아니라 예제로 사용된 스프링이나 JPA에 대한 프레임워크를 설명하는데 더 큰 노력이 들어갈 수 밖에 없습니다. 이 강의는 객체지향 초급자들을 대상으로 설계된만큼 스프링이나 JPA를 기준으로 강의를 구성하면 결국은 예제에서 다루는 스프링이나 JPA를 경험하지 않은 분들은 강의를 따라오실 수 없고 맥락을 전혀 이해하실 수 없으실 거구요.강의에 사용한 라이브러리들은 모르시더라도 강의의 흐름을 쫓아오시는데 큰 무리가 없거나 예제를 구성하는 핵심이 아닌 것으로 선정한 이유이기도 합니다. 그리고 "이 부분을 만약에 업무에 적용시킨다면 어떻게 해야 할까나?”라는 생각을 할거라고 말씀하셨는데, 강의에 있는 원칙들은 실제로 업무에서 코드를 적용할 때 사용할 수 있는 내용들입니다.스프링이나 JPA처럼 특정한 프레임워크를 사용하지 않는건 업무에서 사용하는 예제가 아니라고 생각하시거나 객체지향적인 설계를 실제 업무에 적용하지 않을 경우에 발생할 수 있는 시각적인 차이라고 생각합니다. 스프링 관점에서 특정한 기술에 기반한 설계 방식이 더 궁금하시다면 오히려 이쪽 주제로 더 좋은 강의들이 많이 나와있기 때문에 제 강의보다는 해당 기술을 다룬 강의를 들으시는 것을 추천드립니다.마침 토비님 강의가 새로 나왔으니 이 강의를 한번 보시면 어떨까 싶고(https://inf.run/cKwn3) 저보다 더 잘 아시겠지만 영한님 강의가 이런 측면에서는 정말 탁월합니다.제 강의를 보신 상태에서 두 분의 강의를 보시면 필요한 부분을 충족시킬 수 있으리라 생각합니다. 🙂 설계 원칙편을 스프링과 JPA에 응용한 강의를 만들어 보는 것도 현재 고려하고 있기는 합니다만 바로 진행할지 추가 강의를 만든 후에 진행할지 조금 고민하고 있어요.다만 현재 강의는 설계 원칙 자체를 깊이있게 다루는 것을 목표로 한다는 점을 강조하고 싶습니다.(이게 수강생이 적은 이유일 수도 있겠지만... ㅠㅠ) 애정어린 조언 감사드립니다.
- 2
- 3
- 427
Q&A
동일성과 동등성 설명이 바뀐 것 같습니다.
안녕하세요. 말씀하신 것처럼 identical은 동일성으로, equality는 동등성으로 해석하는게 맞습니다. 제가 강의 자료를 만들면서 혼동한 것 같은데 해당 부분은 수정하도록 할게요. 🙂 자료 양이 많다보니 제가 실수한 부분이 많을 수 있는데 보시다가 이상한 부분이 있으면 편하게 수정 요청해 주시면 감사하겠습니다. 강의 꼼꼼하게 봐주시는 부분도 감사드려요.
- 1
- 2
- 74