인프런 커뮤니티 질문&답변
Game, Player, Room간의 관계에 대한 질문을 드려요
해결된 질문
작성
·
44
1
영호님 안녕하세요! 이번에는 설계원칙편의 6-1장 강의를 듣던 중, 궁금한 부분이 생겨 질문 드립니다~!!
디미터 법칙을 만족시키기 위해 Game 클래스의 showRoom() 메서드 내부의 구현을 리팩토링 하는 과정에서
player.currentRoom()메서드를 통해 Room 객체에 접근하여 name과 description 정보를 반환받도록 하는 부분이 있었습니다.
Q. 혹시 showRoom() 메서드 내부에서 player로부터 Room 객체를 반환 받아 Game객체가 사용하는 것이 아닌, Room에 대한 정보를 Room객체로부터 바로 반환 받도록 하는 것은 어떠신지 궁금합니다!
예를 들어서 Room에 대한 정보를 Room이 책임지도록 하였을 때
public class Player {
private WorldMap worldMap;
private Position position;
...
public String currentRoom() {
return worldMap.roomAt(position).getRoomInfo(); // 디미터 법칙 준수
}
}
public class Room {
private Position position;
private String name;
private String description;
...
public String getRoomInfo() {
return String.format("현재 위치는 %s 입니다. %s\n", name, description);
}
}
Room에 층수 정보와 같이 무언가 추가되는 상황이 발생하더라도 Game에서는 코드를 수정할 필요가 없이 Room 객체 내부에서만 변경하면 될 것 같아보였습니다! 🤔
답변 2
1
테디베어님 안녕하세요.
설계원칙편에서 다시 만나뵈니 반갑네요. 🙂
테디베어님이 주신 질문의 핵심은 화면에 출력할 문구를 구성하는 책임을 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의 필드를 이용해서 데이터베이스에 저장하지만 관심사 측면에서는 다르기 때문에 별도의 객체를 이용해서 객체의 데이터를 저장하는 방식을 떠올리시면 이해가 되실거에요.
이렇게 객체 단위의 응집도를 낮추고 캡슐화를 약화시키는 대신 시스템 전체 관점에서 응집도를 높이는 기법이 자주 사용됩니다.
답변이 되었는지 모르겠네요.
감사합니다. 🙂
이전 답변에서도 말씀드린 것처럼 이 부분은 아키텍처에서의 관심사의 분리(separation of concerns) 개념을 학습하시면 이해가 되실거에요. 🙂
설계원칙편에서 다루는 설계가 클래스 수준에서의 책임 분배와 의존성에 초점을 맞춘다고 한다면 아키텍처는 애플리케이션의 큰 규모에서 책임의 분배와 의존성에 초점을 맞춥니다.
아키텍처 수준에서의 책임을 분배해서 큰 규모의 요소들을 나눈 후 각 요소들 안에서 다시 클래스 수준의 책임을 분배하는 방식으로 이루어진다고 이해하시면 됩니다.
이렇게 하는 이유는 하나의 요소 안에 서로 다른 관심사를 담당하는 로직을 담으면 반대로 응집도가 낮아지기 때문입니다.
예를 들어서 Room을 데이터베이스에 저장하거나, Room 정보를 네트워크를 통해 전송하기 위해 변환해야 하는 로직이 필요하다고 가정해보겠습니다.
객체지향 설계를 단순하게 이해하면 아래처럼 Room의 필드에 접근하는 모든 로직을 Room에 구현해야 한다고 생각할 수 있습니다.
public class Room {
private Position position;
private String name;
private String description;
// UI 문구가 바뀔 때 수정
public String getRoomInfo() {
return String.format("현재 위치는 %s 입니다. %s\\n",
this.name, this.description);
}
// 영속성 메커니즘이 바뀔 때 수정
public void saveRoom() {
String sql = "insert into room(x, y, name, description) " +
"values(?, ?, ?, ?)";
DataBaseUtil.inert(this.sql, this.position.x(),
this.position().y(), this.name, this.description);
}
// 네트워크에 전송할 데이터가 바뀔 때 수정
public RoomDTO toRoomDTO() {
return new RoomDTO(this.position, this.name,
this.description);
}
}
이 클래스는 너무 많은 관심사에 관련되어 있기 때문에 응집도가 낮고 변경에 취약하며 결합도가 높습니다.
다시 말해서 변경할 이유가 너무 많습니다.
UI의 출력 문구가 변경되거나, 데이터베이스에 저장하는 방식이 바뀌거나, 네트워크 전송에 필요한 데이터가 변경되거나, Room의 도메인 로직이 변경될 때도 변경됩니다.
이 Room 클래스는 단일 책임 원칙을 위반하기 때문에 각각의 관심사를 나누는게 전체적인 관점에서 더 좋은 설계인거죠.
그리고 질문에서 다음과 같이 적어주셨는데요.
예를 들어, "Room 정보로 층수, 보물 정보와 같은 정보도 추가해주세요." 라는 요구사항이 추가적으로 들어오는 경우가 발생한다면 Game 클래스의
showRoom()메서드에서도 Room 객체에 추가되는 상태 정보들을 반영하는 코드가 추가될 것 같습니다
Game 클래스가 Room 클래스에 의존하도록 설계한 이유는 Room이 변경되었을 때 Game 클래스도 함께 변경될 것을 감안했기 때문입니다.
오히려 의존성의 방향과 반대로 화면 출력을 담당하는 Game의 필요때문에 Room이 변경되는게 이슈일거에요.
설계에서는 지역 최적화보다 전역 최적화가 더 중요합니다.
이전 답변 마지막에 “이렇게 객체 단위의 응집도를 낮추고 캡슐화를 약화시키는 대신 시스템 전체 관점에서 응집도를 높이는 기법이 자주 사용됩니다”라고 적은 부분이 이런 뜻이라고 생각하시면 됩니다.
따라서 어떤 클래스의 필드를 사용하는 로직을 단순히 클래스로 이동시키기 전에 큰 규모에서 서로 다른 이유로 변경되는 관심사를 한 클래스에 섞는 것은 아닌지 고민해보시는게 좋습니다.
답변이 되었는지 모르겠네요. 😊
감사합니다.
😯😯😯😯😯 너무 답변이 잘 되었습니다!
제가 너무 지엽적으로 단순하게 생각했던 것 같네요.. 🥲
또 하나 알게 된 것 같습니다. 감사합니다!!
아키텍처에서의 관심사의 분리에 대해 학습해보고, 책임을 이동할 때에는 전역 최적화에 대한 고민도 해보겠습니다!
0
안녕하세요, 인프런 AI 인턴입니다. 테디베어님!
디미터 법칙과 관련하여 Game, Player, 그리고 Room 간의 관계를 설계하며 고민한 점을 공유해주셔서 감사합니다. 설계 원칙은 객체지향 프로그래밍에서 매우 중요한 주제이며, 특히 디미터 법칙은 객체 간의 낮은 결합도와 높은 응집도를 유지하기 위한 좋은 기준이 됩니다.
설계 고려사항
디미터의 법칙
디미터 법칙은 “메서드는 자신이 갖고 있는 객체와만 상호작용해야 한다”는 원칙입니다. 이 원칙을 적용하면 객체 간의 결합도를 낮추고, 시스템의 유지 보수성을 높일 수 있습니다.
제안한 접근 방식
테디베어님께서 제안하신 것처럼 Room 객체가 스스로 자신의 정보를 반환하도록 설계하는 방식은 디미터 법칙을 잘 따르는 방식 중 하나입니다. 이 접근 방식은 다음과 같은 장점이 있습니다:
- 객체의 책임 분리:
Room객체가 자신의 정보를 책임지므로Game클래스에서Room의 구현 세부 정보를 알 필요가 없습니다.
- 유연성 증대: 만약
Room객체에 새로운 정보가 추가되더라도,Room클래스만 수정하면 되기 때문에 코드 유지보수가 용이합니다.
- 낮은 결합도:
Game클래스는 단순히Player로부터Room정보를 얻어오고, 이 정보를 그대로 사용합니다. 따라서 각 클래스가 독립적으로 변화할 수 있는 여지가 커집니다.
테디베어님께서 작성해주신 코드는 이러한 디미터 법칙의 장점을 잘 살리고 있어 좋은 설계라고 생각합니다. 객체의 책임을 명확히 하고, 시스템 변경 시의 영향을 최소화하려는 노력은 특히 훌륭합니다.
더구나, 향후 Room에 관련된 정보를 확장해야 할 때, Room 클래스 내부에서 변경하면 되기 때문에, 코드의 일관성을 유지할 수 있습니다.
추가 참고링크
혹시 추가적으로 객체지향 설계와 관련된 다양한 관점이나 패턴을 공부해보시고자 한다면 다음 질문 및 답변을 참고해 보시면 좋을 것 같습니다:
저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.





영호님 답변 감사합니다!
저도 Game클래스의 화면에 출력하는 문구를 구성하는 책임과 Player가 담당하는 로직은 분리되어야 한다고 생각합니다.
그리고 문구 내용의 수정이 필요하다면, Game 클래스의
showRoom()메서드만 수정하면 되는 부분도 이해하였습니다!다만 Game 클래스가 화면에 출력하는 문구를 구성할 때, Room 객체의 데이터를 사용하고 있다는 점에서
역으로 Room 객체에 데이터를 추가해야 하는 경우가 발생한다면, Game 클래스의
showRoom()메서드에 이에 따른 파급효과가 생기는 것이 아닌가 싶었습니다.이런 이유로 Room 정보에 대한 데이터를 가장 많이 알고 있는 Room 객체에 책임을 이동시키는 것을 생각해보게 되었는데, 제가 잘못 생각한 것일까요? 🤔