우리는 학생이라는 객체, 그리고 학생을 묶은 그룹이라는 객체를 가지고 있습니다.
public class Student {
private String name;
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
// 정적 팩토리 메서드
public static Student of(String name){
return new Student(name);
}
}
import java.util.*;
public class Group {
private String groupName;
private List<Student> member = new ArrayList<>();
public Group(String groupName) {
this.groupName = groupName;
}
public void addMember(List<Student> students){
member.addAll(students);
}
public List<Student> getMembers() {
return member;
}
public String getGroupName() {
return groupName;
}
}
실제 학생과 그룹 데이터를 다음과 같이 생성했습니다. 우리의 목적은 이름이 "O"인 학생이 어느 그룹에 속하는지 확인하는 것입니다.
import java.util.*;
public class Main {
public static void main(String[] args) {
Group groupA = new Group("GROUP-A");
groupA.addMember(List.of(Student.of("A"),
Student.of("B"),
Student.of("C"),
Student.of("D")));
Group groupB = new Group("GROUP-B");
groupB.addMember(List.of(Student.of("E"),
Student.of("F"),
Student.of("G"),
Student.of("H")));
Group groupC = new Group("GROUP-C");
groupC.addMember(List.of(Student.of("I"),
Student.of("J"),
Student.of("K"),
Student.of("L")));
Group groupD = new Group("GROUP-D");
groupD.addMember(List.of(Student.of("M"),
Student.of("N"),
Student.of("O"),
Student.of("P")));
List<Group> totalGroup = List.of(groupA, groupB, groupC, groupD);
// O라는 학생이 어느 그룹에 속하는지 알고 싶습니다.
String targetName = "O";
}
이제 이름이 targetName인 학생이 어느 그룹에 속하는지 확인하는 메서드를 작성해 보겠습니다.
public static Group findGroupWithTargetMemberIncluded(String targetName, List<Group> totalGroup){
for (Group group: totalGroup) { // 각각의 그룹에 대해
for(Student student: group.getMembers()){ // 각 그룹의 학생에 대해
if(student.getName().equals(targetName)){ // 만약 학생의 이름이 우리가 찾는 학생의 이름이면
return group;
}
}
}
return null;
}
직전에 작성한 findGroupWithTargetMemberIncluded메서드는 Student객체를 직접 노출하고 있습니다. 이는 캡슐화 측면에서 좋지 못하며, 메서드는 역할을 수행하기 위해 Group뿐만 아니라 Student도 알아야 하는 상황입니다. 이를 해결하기 위해 코드를 수정해 보겠습니다.
Group 객체에 다음과 같은 메서드를 추가했습니다.
public List<String> getMemberNames(){
return member.stream()
.map(Student::getName)
.toList();
}
새롭게 추가된 메서드를 통해 다음과 같은 코드를 수정할 수 있습니다.
public static Group findGroupWithTargetMemberIncluded(String targetName, List<Group> totalGroup){
for (Group group: totalGroup) {
for(String studentName: group.getMemberNames()){
if(studentName.equals(targetName)){
return group;
}
}
}
return null;
}
Student객체를 findGroupWithTargetMemberIncluded메서드에서 직접적으로 노출시키지 않습니다. findGroupWithTargetMemberIncluded는 Student를 몰라도 정상적으로 동작합니다.for문 안에 for문 안에 if문이 있는 중첩된 구조라는 점도 문제입니다. 지금처럼 단순화된 예제에서는 한눈에 코드를 파악할 수 있지만 만약 복잡한 코드가 for-for-if의 중첩구조를 가진다면 이를 이해하는 건 쉽지 않을 겁니다.
여기서는 '모듈은 자신이 호출한 객체의 내부구조를 몰라야 한다'는 디미터 법칙과 묻지 말고 시켜라(Tell, Don’t ASK)라는 원칙을 적용시킬 수 있습니다.
지금 코드는 getMemberNames메서드를 통해 Group객체의 내부 값인studentName을 직접 가져오고 있습니다. 이는 Group의 내부구조를 findGroupWithTargetMemberIncluded가 알게 됨을 의미합니다.
또한 studentName이 targetName과 동일한지를 묻고있습니다.
Group이 특정 이름의 학생이 본인 그룹에 존재하는지 확인하는 역할을 지니면 외부에 내부구조를 노출시키지 않을 수 있으며, 외부에서는 Group객체에게 targetName의 학생이 존재하는지 알아오도록 지시할 수 있습니다.
public boolean hasMember(String targetName){
for (String studentName: getMemberNames()) {
if(studentName.equals(targetName)) return true;
}
return false;
}
targetName이름을 가진 학생이 있는지 확인하는 책임을 가지게 됐습니다.public static Group findGroupWithTargetMemberIncluded(String targetName, List<Group> totalGroup){
for (Group group: totalGroup) {
if(group.hasMember(targetName)){
return group;
}
}
return null;
}
hasMember메서드를 통해 Group에게 targetName이 존재하는지를 알아오라고 지시하고 있습니다. for-for-if구조에서 for-if구조로 변경돼 가독성도 더욱 좋아졌습니다.)
묻는 것과시키는 것의 차이를 완벽히 이해하기가 어려웠습니다.
저는묻는 것은 물음의 답을 통해질문자가 결과를 만들어야 한다면,시키는 것은 결과 자체를 전달받는 거라고 생각했습니다.
studentName.equals(targetName)는 Group으로부터studentName이라는 답을 얻은 뒤findGroupWithTargetMemberIncluded메서드가 직접 equals를 통해 존재여부를 판단하지만,
group.hasMember(targetName)는 Group으로부터 존재여부 자체를 반환받기 때문에findGroupWithTargetMemberIncluded메서드에서 별도의 판단이 필요하지 않다고 이해했습니다.
Group의 hasMember메서드는 Stream을 통해 더욱 간단하고 직관적인 코드로 변경될 수 있습니다.
public boolean hasMember(String targetName){
for (String studentName: getMemberNames()) {
if(studentName.equals(targetName)) return true;
}
return false;
}
public boolean hasMember(String targetName){
return getMemberNames()
.stream()
.anyMatch(targetName::equals);
}