객체 지향 설계 SOLID 원칙

두문자 약어 이름 개념
S SRP 단일 책임 원칙
Single Responsibility Principle
한 클래스는 하나의 책임을 가져야 한다.
O OCP 개방-폐쇄 원칙
Open-Closed Principle
소프트웨어 개체(클래스, 모듈 등)는 확장에는 열려있으나, 수정에는 닫혀 있어야 한다.
L LSP 리스코프 치환 원칙
Liskov Substitution Principle
객체의 정확성을 깨뜨리지 않으면서 상위클래스의 객체를 하위클래스의 객체로 바꿀 수 있어야한다.
I ISP 인터페이스 분리 원칙
Interface Segregation Principle
범용적인 인터페이스보다 클라이언트를 위한 인터페이스 여러개가 더 낫다.
D DIP 의존관계 역전 원칙
Dependency Inversion Principle
추상화에 의존해야지, 구체화에 의존하면 안된다.

 

 

 

의존관계 역전 원칙

DIP 원칙이란 사용자는 Class를 직접 참조하는것이 아니라 그 Class의 추상클래스 또는 인터페이스를 참조해야한다는 원칙입니다. 이 원칙을 따르면, 상위 계층이 하위 계층에 의존하는 의존관계를 역전(반전)시킴으로써 상위 계층이 하위 계층의 구현으로부터 독립되게 할 수 있습니다.

 

  1. 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
  2. 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.

 

 

 

 

1. DIP 원칙 위반

public class Robot {

}

public class RacingCar {
    
}

public class Game {
    
}

public class Kid {

    private Robot robot;

    public void setRobot(Robot robot) {
        this.robot = robot;
    }
    
    public void getToyType() {
        System.out.println(robot.toString());
    }
}

 

Kid 클래스에는 한가지의 장난감이 들어가야합니다. 하지만 장난감은 Robot만 존재하지 않습니다. 만약 장난감을 변경해야한다면 Kid(사용자) 클래스를 수정해야합니다.

 

즉 이 코드는 하위 모듈을 의존하고 있습니다.

 

 

 

2. DIP 원칙 적용

public interface Toy { ... }

public class Robot implements Toy { ... }

public class RacingCar implements Toy { ... }

public class Game implements Toy { ... }


public class Kid {

    private Toy toy;

    public void setToy(Toy toy) {
        this.toy = toy;
    }

    public void getToyType() {
        System.out.println(toy.toString());
    }
}

 

Kid 클래스가 Toy 인터페이스를 의존하도록 한다면 Kid 클래스의 변경 없이 OCP 원칙 또한 지키게 되었습니다.

객체 지향 설계 SOLID 원칙

두문자 약어 이름 개념
S SRP 단일 책임 원칙
Single Responsibility Principle
한 클래스는 하나의 책임을 가져야 한다.
O OCP 개방-폐쇄 원칙
Open-Closed Principle
소프트웨어 개체(클래스, 모듈 등)는 확장에는 열려있으나, 수정에는 닫혀 있어야 한다.
L LSP 리스코프 치환 원칙
Liskov Substitution Principle
객체의 정확성을 깨뜨리지 않으면서 상위클래스의 객체를 하위클래스의 객체로 바꿀 수 있어야한다.
I ISP 인터페이스 분리 원칙
Interface Segregation Principle
범용적인 인터페이스보다 클라이언트를 위한 인터페이스 여러개가 더 낫다.
D DIP 의존관계 역전 원칙
Dependency Inversion Principle
추상화에 의존해야지, 구체화에 의존하면 안된다.

 

 

 

인터페이스 분리 원칙

ISP 원칙이란 범용적인 인터페이스보다 사용자가 실제로 사용하는 Interface를 만들어야한다는 의미입니다.

즉, 자신이 이용하지 않는 메소드에 의존하지 않아야하는 원칙인데요. 큰 덩어리의 인터페이스들을 구체적이고 작은 단위들로 분리시킴으로써 사용자들이 꼭 필요한 메소드들만 이용할 수 있게 하는 것이 중요합니다.

 

 

 

 

 

인터페이스 분리 원칙의 예

 

 

1. ISP 위반 예제

public interface Phone {

    void call(String number);
    void message(String number, String text);
    void app();
    void camera();

}

public class IPhone15 implements Phone{

    @Override
    public void call(String number) {

    }

    @Override
    public void message(String number, String text) {

    }

    @Override
    public void app() {

    }

    @Override
    public void camera() {

    }
}

 

스마트폰을 추상화했습니다. 스마트폰에는 많은 기능들이 포함되어있습니다.

여기서 아이폰을 구현한다면 모든 기능들을 오버라이드 하면 문제가 없어보입니다.

 

만약 버전이 올라가면서 기능이 추가되거나

이전 버전의 핸드폰을 추가할때 지원하지 않는 기능은 어떻게 될까요?

public class FolderPhone implements Phone{

    @Override
    public void call(String number) {
        
    }

    @Override
    public void message(String number, String text) {

    }

    @Override
    public void app() {
        throw new NotSupportedException();
    }

    @Override
    public void camera() {

    }
}

public class IPhone20 implements Phone{

    @Override
    public void call(String number) {

    }

    @Override
    public void message(String number, String text) {
        throw new NotSupportedException();
    }

    @Override
    public void app() {

    }

    @Override
    public void camera() {

    }
}

 

이전 버전의 폴더폰은 app 기능이 없어서 예외를 던지고, 이후 버전이 올라가면서 더이상 message 기능을 사용하지 않는다면 이후 기능들에서 모든 message는 예외를 던지게됩니다.

 

2. ISP 적용

public interface Phone {

    void call(String number);
}

public interface Message {

    void message(String number, String text);
}

public interface App {

    void app();
}

public interface Camera {

    void camera();
}

 

각각의 기능별로 인터페이스를 분리했습니다.

 

 

public class FolderPhone implements Phone, Message, Camera{

    @Override
    public void call(String number) {
        
    }

    @Override
    public void message(String number, String text) {

    }

    @Override
    public void camera() {

    }
}

public class IPhoneXR implements Phone, Message, App, Camera{

    @Override
    public void call(String number) {

    }

    @Override
    public void message(String number, String text) {

    }

    @Override
    public void app() {

    }

    @Override
    public void camera() {

    }
}

public class IPhone20 implements Phone, App, Camera{

    @Override
    public void call(String number) {

    }

    @Override
    public void app() {

    }

    @Override
    public void camera() {

    }
}

 

작은 단위로 인터페이스가 분리되어 지원되는 기능만을 구현할 수 있게되었습니다.

 

 

 

객체 지향 설계 SOLID 원칙

두문자 약어 이름 개념
S SRP 단일 책임 원칙
Single Responsibility Principle
한 클래스는 하나의 책임을 가져야 한다.
O OCP 개방-폐쇄 원칙
Open-Closed Principle
소프트웨어 개체(클래스, 모듈 등)는 확장에는 열려있으나, 수정에는 닫혀 있어야 한다.
L LSP 리스코프 치환 원칙
Liskov Substitution Principle
객체의 정확성을 깨뜨리지 않으면서 상위클래스의 객체를 하위클래스의 객체로 바꿀 수 있어야한다.
I ISP 인터페이스 분리 원칙
Interface Segregation Principle
범용적인 인터페이스보다 클라이언트를 위한 인터페이스 여러개가 더 낫다.
D DIP 의존관계 역전 원칙
Dependency Inversion Principle
추상화에 의존해야지, 구체화에 의존하면 안된다.

 

 

 

리스코프 치환 원칙

리스코프 치환 원칙이란 부모 객체와 자식 객체가 있을 때 부모 객체를 호출하는 동작에서 자식 객체로 대체하였을때 정확성을 깨뜨리지 않으면서 완전히 대체 가능해야한다는 원칙입니다.

쉽게 말해 부모객체에서 자식객체로 변경되었어도 문제가 생기지 않아야 한다는 뜻입니다.

 

 

 

LSP 위반 예제

public class Counter {

    private List<Member> memberList = new ArrayList<>();
    private int count = 0;

    public void addAll(List<Member> list) {
        for (Member member : list) {
            add(member);
        }
    }

    public void add(Member member) {
        memberList.add(member);
        addCount();
    }

    protected void addCount() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class Main {

    public static void main(String[] args) {

        Counter counter = new Counter();
        counter.add(new Member());

        List<Member> members = List.of(new Member(), new Member(), new Member(), new Member(), new Member());
        counter.addAll(members);


        System.out.println(counter.getCount()); // 6
    }

}

 

List의 개수를 count를 하는 클래스를 만들었습니다. Counter의 addAll은 자신의 클래스내에 존재하는 add를 호출하여 카운트를 합니다.

 

public class ArrayCounter extends Counter{

    @Override
    public void addAll(List<Member> list) {
        super.addAll(list);
        for (Member member : list) {
            addCount();
        }
    }

}

public class Main {

    public static void main(String[] args) {

        ArrayCounter counter = new ArrayCounter();
        counter.add(new Member());

        List<Member> members = List.of(new Member(), new Member(), new Member(), new Member(), new Member());
        counter.addAll(members);
        
        System.out.println(counter.getCount()); // 11
    }

}

addAll 메소드안에서 add 메소드르 호출하는 것을 몰랐던 개발자는 List의 사이즈를 카운트 하기 위해 addCount()메소드를 호출합니다.따라서 ArrayCounter에서 add가 11번이 호출되었고,

그 결과 Counter 객체의 결과는 6, ArrayCounter 객체의 결과는 11이 나왔습니다. 이는 부모객체를 자식객체로 대체되었을 때 완전히 대체되지 않으므로 LSP 위반입니다.

 

 

결론

LSP 원칙을 잘 적용한 예제는 우리가 자주 사용하는 컬렉션 프레임워크입니다.

        Collection<Integer> collection = new HashSet<>();
        collection.add(1);

        collection = new TreeSet<>();
        collection.add(2);

Collection 안에서 Hashset 자료형을 사용하다가 TreeSet으로 변경하더라도 add() 메소드가 동작하는데에는 전혀 문제가 되지 않습니다.

 

리스코프 치환 원칙은 자바를 배우면서 사용해온 다형성을 규칙으로서 문서화한 것이 LSP 원칙이라고 생각하시면 됩니다.

객체 지향 설계 SOLID 원칙

두문자 약어 이름 개념
S SRP 단일 책임 원칙
Single Responsibility Principle
한 클래스는 하나의 책임을 가져야 한다.
O OCP 개방-폐쇄 원칙
Open-Closed Principle
소프트웨어 개체(클래스, 모듈 등)는 확장에는 열려있으나, 수정에는 닫혀 있어야 한다.
L LSP 리스코프 치환 원칙
Liskov Substitution Principle
객체의 정확성을 깨뜨리지 않으면서 상위클래스의 객체를 하위클래스의 객체로 바꿀 수 있어야한다.
I ISP 인터페이스 분리 원칙
Interface Segregation Principle
범용적인 인터페이스보다 클라이언트를 위한 인터페이스 여러개가 더 낫다.
D DIP 의존관계 역전 원칙
Dependency Inversion Principle
추상화에 의존해야지, 구체화에 의존하면 안된다.

 

 

OCP 개방-폐쇄 원칙이란

확장에는 열려있고, 수정에는 닫혀있어야 한다.

 

개방-폐쇄 원칙이란 기존의 코드를 변경하지 않으면서(Closed), 기능을 추가할 수 있도록(Open) 설계해야 한다는 원칙입니다.

 

 

 

 

 

개방-폐쇄 원칙의 예

추첨기를 만들어 보겠습니다.

 

 

1. OCP 적용 전

public class LotteryMachine {

    private final RandomStrategy strategy;
    
    public LotteryMachine(RandomStrategy strategy) {
        this.strategy = strategy;
    }

    public User select(List<User> userList) {

        if (strategy == 랜덤) {
            Collections.shuffle(userList);
        }
        if (strategy == 오름차순) {
            userList.sort(Comparator.comparingInt(User::getScore));
        }
        if (strategy == 내림차순) {
            userList.sort((o1, o2) -> Integer.compare(o2.getScore(), o1.getScore()));
        }

        return userList.get(0);
    }
}

 

이 추첨기에서 추첨방식을 추가한다면 기존의 코드를 수정하게 됩니다. 즉 OCP 위반입니다. 기존의 코드를 수정하지 않고 추첨방식을 추가하려면 어떻게 해야 할까요? 자바의 다형성을 이용하여 추첨방식을 추상화하여 사용하는 겁니다.

 

 

2. OCP 적용 후

public class LotteryMachine {

    private final ShuffleStrategy strategy;

    public LotteryMachine(ShuffleStrategy strategy) {
        this.strategy = strategy;
    }

    public User select(List<User> userList) {
        
        strategy.shuffle(userList);

        return userList.get(0);
    }
}

 

 

public interface ShuffleStrategy {

    void shuffle(List<User> userList);
}

public class RandomStrategy implements ShuffleStrategy{
    @Override
    public void shuffle(List<User> userList) {
        Collections.shuffle(userList);
    }
}

public class AscStrategy implements ShuffleStrategy{
    @Override
    public void shuffle(List<User> userList) {
        userList.sort(Comparator.comparingInt(User::getScore));
    }
}

public class DescStrategy implements ShuffleStrategy{
    @Override
    public void shuffle(List<User> userList) {
        userList.sort((o1, o2) -> Integer.compare(o2.getScore(), o1.getScore()));
    }
}

 

 

추첨기는 인터페이스화된 ShuffleStrategy를 의존하도록 하고 생성자를 통해서 구현체를 주입해주었습니다.

이렇게 하면 추첨방식을 추가하더라도 기존의 추첨기 클래스의 수정이 일어나지 않습니다.

 

3. 추첨기의 확장

public class NoneStrategy implements ShuffleStrategy{
    @Override
    public void shuffle(List<User> userList) {
        
    }
}

섞지 않는 추첨방식을 손쉽게 확장했습니다.

객체 지향 설계 SOLID 원칙

두문자 약어 이름 개념
S SRP 단일 책임 원칙
Single Responsibility Principle
한 클래스는 하나의 책임을 가져야 한다.
O OCP 개방-폐쇄 원칙
Open-Closed Principle
소프트웨어 개체(클래스, 모듈 등)는 확장에는 열려있으나, 수정에는 닫혀 있어야 한다.
L LSP 리스코프 치환 원칙
Liskov Substitution Principle
객체의 정확성을 깨뜨리지 않으면서 상위클래스의 객체를 하위클래스의 객체로 바꿀 수 있어야한다.
I ISP 인터페이스 분리 원칙
Interface Segregation Principle
범용적인 인터페이스보다 클라이언트를 위한 인터페이스 여러개가 더 낫다.
D DIP 의존관계 역전 원칙
Dependency Inversion Principle
추상화에 의존해야지, 구체화에 의존하면 안된다.

 

 

 

SRP 단일 책임 원칙이란

한 클래스는 하나의 책임을 가져야 한다.

 

단일 책임 원칙이란 모든 객체는 하나의 책임만 가지며, 객체는 그 책임을 완전히 캡슐화 해야한다는 원칙입니다. 한개의 객체에 책임이 많아질 수록 변경이 자주 일어나며 객체간의 의존성과 결합도가 높아질 것입니다.

 

 

 

단일 책임 원칙의 예

문서를 작성하고 프린터로 출력하는 객체를 만들어 보겠습니다.

 

 

1. SRP 적용 전

public class Printer {
    
    private String text;

    public void write(String text) {
        this.text = text;
    }

    public void print() {
        System.out.println(text);
    }

}

public class Main {

    public static void main(String[] args) {
        Printer printer = new Printer();

        printer.write("보고서 작성");
        printer.print(); // 보고서 작성

    }
}

 

언뜻보면 잘 만들어진 것 같지만 Printer는 2가지의 책임을 가지고 있습니다. 현실에서 생각해보아도 프린터에서 글을 쓰고 용지를 선택하지 않습니다. 프린터는 출력하는 일만 하고, 문서에서 글을 쓰는 일만 합니다. 

 

 

2. SRP 적용 후

public class Printer {

    private Paper paper;

    public Printer(Paper paper) {
        this.paper = paper;
    }

    public void print() {
        System.out.println(paper);
    }

}

public class Paper {

    private String text;

    public void write(String text) {
        this.text = text;
    }

    @Override
    public String toString() {
        return text;
    }
}

public class Main {

    public static void main(String[] args) {

        Paper paper = new Paper();
        paper.write("보고서 작성");

        Printer printer = new Printer(paper);
        printer.print(); // 보고서 작성

    }
}

 

Printer와 Paper의 책임을 분리했습니다. Printer는 출력기능만 존재하고,  Paper는 문서를 작성하는 기능만 존재합니다.

객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.

 

목차

  1. 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
  2. else 예약어를 사용하지 않는다.
  3. 모든 원시 값과 문자열을 포장한다.
  4. 일급 컬렉션을 쓴다.
  5. 한 줄에 점을 하나만 찍는다.
  6. 줄여 쓰지 않는다 ( 축약 금지 )
  7. 모든 엔티티를 작게 유지한다.
  8. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  9. Getter / Setter / Property를 쓰지 않는다.

 

 

Getter / Setter / Property를 쓰지 않는다.


벌써 마지막 9번째 입니다. 여전히 마지막까지 추상적인 말만 늘어놓네요.

이 규칙은 정말로 getter setter를 쓰지말라는 의미가 아닙니다. 객체 내부에 어떤 속성이 있는지 외부에서 알지 못하게 하는 캡슐화에 초점을 맞춰야합니다.

 

TDA 원칙이라는것이 있습니다. Tell, Don't ask, 즉 정보를 묻지말고 하고싶은걸 하라는건데요.

 

 

 

예시로 값이 짝수인지 확인하는 로직을 만들어보겠습니다.

public class Number {

    private int number;

    public Number(int number) {
        this.number = number;
    }

    public int getNumber() {
        return number;
    }
    
}

public class Main {

    public static void main(String[] args) {

        Number number = new Number(5);
        int intNumber = number.getNumber();

        if (intNumber%2 == 0) {
            System.out.println("짝수임");
        } else {
            System.out.println("짝수아님");
        }
    }
}

 

number.getNumber()를 호출하면서 객체 내부의 값이 노출되었고, 그 값을 로직에 사용했습니다. 확실히 캡슐화가 깨졌습니다.

짝수인지 판단하는 로직을 Number 객체 안에 숨겨보겠습니다.

 

 

public class Number {

    private final int number;

    public Number(int number) {
        this.number = number;
    }

    public void isEven() {
        if (number%2 == 0) {
            System.out.println("짝수임");
        } else {
            System.out.println("짝수아님");
        }
    }
}

public class Main {

    public static void main(String[] args) {

        Number number = new Number(5);
        number.isEven();

    }
}

 

Getter가 사라져 Number 객체안에 값을 알지 못하게되었습니다.

 

그렇다면 Gette는 절대 사용하면 안되는것일까요? 당연히 아닙니다. 경우에 따라 다릅니다.

객체의 값을 외부로 표현해주어야할 경우에는 Getter를 사용해야합니다.

 

public void printNumber() {
    System.out.println("현재 값은 " + number + " 입니다.");
}

 

 

 

 

 

Setter의 경우도 마찬가지입니다.

 

public class Main {

    public static void main(String[] args) {

        Number number = new Number(5);
        number.setNumber(number.getNumber() + 2);

    }
}

현재값에 2를 더하는 로직입니다. setter를 사용하기 위해 getter + 2를 하는건 상당히 보기 안좋습니다.

 

public void addNumber(int number) {
    this.number += number;
}


public class Main {

    public static void main(String[] args) {

        Number number = new Number(5);
        number.addNumber(2);

    }
}

 

핵심은 캡슐화라는것을 명심해주세요.

객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.

목차

  1. 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
  2. else 예약어를 사용하지 않는다.
  3. 모든 원시 값과 문자열을 포장한다.
  4. 일급 컬렉션을 쓴다.
  5. 한 줄에 점을 하나만 찍는다.
  6. 줄여 쓰지 않는다 ( 축약 금지 )
  7. 모든 엔티티를 작게 유지한다.
  8. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  9. Getter / Setter / Property를 쓰지 않는다.
3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.

 

인스턴스 변수가 많으면 많을 수록 응집도가 떨어진다. 라고 해석하면 좋을 것 같습니다. 여기서 말하는 인스턴스 변수는 기본형을 의미하는것 같습니다. 확실하지는 않아요. 사실 이번편은 3편 원시값과 문자열을 포장한다와 맥락이 상당히 유사합니다.

 

 

public class Student {

    private String name;
    private String age;
    private String score;
    
}

Student 클래스안에 3개의 인스턴스 변수가 존재합니다.

 

이를 

public class Student {

    private Name name;
    private Age age;
    private Score score;
    
}

이렇게 원시값과 문자열을 포장하거나

 

public class Student {

    private UserInfo info;
    private Score score;
    
}

public class UserInfo {

    private String name;
    private int age;

}

 

DB 제2 정규화 과정처럼 부분적 종속 변수를 묶거나 하라는 의미같습니다.

 

사실 조금 이해가 안되는 부분도 많아서 인스턴스변수가 많아지면 응집도가 낮아진다 라고 이해하고 넘어가도 좋을것같습니다.

객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.

목차

  1. 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
  2. else 예약어를 사용하지 않는다.
  3. 모든 원시 값과 문자열을 포장한다.
  4. 일급 컬렉션을 쓴다.
  5. 한 줄에 점을 하나만 찍는다.
  6. 줄여 쓰지 않는다 ( 축약 금지 )
  7. 모든 엔티티를 작게 유지한다.
  8. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  9. Getter / Setter / Property를 쓰지 않는다.

 

모든 엔티티를 작게 유지한다.

 

 

이 규칙은 클래스는 50줄 이하로 유지하고, 패키지는 10개 이하의 파일만 가져야한다. 라고 책에서 설명되어있습니다. 가능한가? 싶기도 하지만 잘 생각해보면 '코드가 길어진다는 것은 클래스가 한가지 이상의 일을 하고있을 확률이 높다' 라고도 해석할 수 있을 것같습니다.

그러니 50줄 이하의 클래스, 10개 이하의 패키지에 초점을 맞추기 보단 최대한 엔티티를 작게 유지할 수 있도록 노력하는것이 제가 해석하는 바입니다.

 

 

그냥 말로만 하고 넘어가긴 아쉬우니 지금까지 만들었던 StudentList 객체가 규칙을 잘 지키고 있는지 확인해보겠습니다.

public class StudentList {

    private final List<Student> students;

    public StudentList() {
        this.students = new ArrayList<>();
    }

    public void addStudent(Student student) {
        validateStudentsSize();
        validateStudentName(student);

        students.add(student);
    }

    public Student getTopScoreStudent() {
        Student topStudent = null;
        for (Student student : students) {
            if (topStudent == null) {
                topStudent = student;
                continue;
            }

            topStudent = topStudent.compareScore(student);
        }
        return topStudent;
    }

    public List<Student> getStudents() {
        return Collections.unmodifiableList(students);
    }

    public double getAverageAge() {
        return students.stream()
            .mapToInt(student -> student.getAge().getAge()) // 쉿!
            .average()
            .orElse(0);
    }

    private void validateStudentName(Student student) {
        String name = student.getName().getName();

        for (Student studentFor : students) {
            String nameFor = studentFor.getName().getName();

            if (name.equals(nameFor)) {
                throw new IllegalStudentsException("중복된 이름이 존재합니다.");
            }
        }
    }

    private void validateStudentsSize() {
        if (students.size() > 10) {
            throw new IllegalStudentsException("최대 학생 수는 10명입니다.");
        }
    }

}

 

라인을 확인해보니 58줄.. 8줄 오버되고 있네요. 저번편을 보신분들이라면 눈에 거슬리는 메서드가 있을겁니다.

바로 validateStudentName() 메서드입니다. 아.. '코드가 길어진다는 것은 클래스가 한가지 이상의 일을 하고있을 확률이 높다' 이 말이 틀린게 하나 없었네요. 대체 어디까지 보신겁니까....

 

 

// 변경 전
private void validateStudentName(Student student) {
    String name = student.getName().getName();

    for (Student studentFor : students) {
        String nameFor = studentFor.getName().getName();

        if (name.equals(nameFor)) {
            throw new IllegalStudentsException("중복된 이름이 존재합니다.");
        }
    }
}

// 변경 후
private void validateStudentName(Student student) {
    for (Student studentFor : students) {
        student.checkDistinctName(studentFor);
    }
}

 

 자세한 내용은 5편에서 다뤘기 때문에 생략하도록 하겠습니다.

 

StudentList의 길이가 58줄에서 52줄로 6줄 줄어들었습니다. 강박적으로 줄이라면 더욱 줄일 수 있겠지만 이 정도만해도 최대한 엔티티를 작게 유지하도록 노력한것이라고 봐도 무방하겠죠?

 

객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.

목차

  1. 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
  2. else 예약어를 사용하지 않는다.
  3. 모든 원시 값과 문자열을 포장한다.
  4. 일급 컬렉션을 쓴다.
  5. 한 줄에 점을 하나만 찍는다.
  6. 줄여 쓰지 않는다 ( 축약 금지 )
  7. 모든 엔티티를 작게 유지한다.
  8. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  9. Getter / Setter / Property를 쓰지 않는다.
축약 금지

 

클래스, 메서드, 변수명의 이름을 줄여쓰지 말자는 규칙입니다.

 

이전시간에 예제로 사용되었던 StudentList 객체를 가져오겠습니다.

 

public class StudentList {

    private final List<Student> students;

    public StudentList() {
        this.students = new ArrayList<>();
    }

    public void add(Student student) {
        validateStudentsSize();
        validateStudentName(student);

        students.add(student);
    }
    
    // 이하 생략 ...
}

 

객체지향 생활체조 원칙 3. 모든 원시값과 문자열을 포장한다. 편 부터 읽으신분들은 이 코드를 보자마자 add 메서드는 Student 객체를 검증하고 List에 넣는 메서드라는것을 아실겁니다. 하지만 처음보는사람이 이 코드를 본다면 어떤 역할을 하는지 유추하기에는 어려움이 있죠. 물론 예제코드가 어렵지 않아 예상이 가겠지만 코드가 더 복잡해지고 일이 많아진다면 더욱 어려워질 것입니다.

 

따라서 이름을 바꿔보겠습니다.

 

public class StudentList {

    private final List<Student> students;

    public void addStudent(Student student) {
        validateStudentsSize();
        validateStudentName(student);

        students.add(student);
    }
    
    // 이하 생략 ...
}

 

이렇게만 바꿔주어도 의미가 명확해집니다.

 

 

축약금지의 의미는 정말 간단합니다. 일에 대한 명확한 이름을 지정해주어 혼동을 주지 말라는 의미입니다.

객체지향 생활체조 원칙은 소트웍스 앤솔러지(ThoughtWorks Anthology) 라는 책에 나오는 원칙이다.

목차

  1. 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
  2. else 예약어를 사용하지 않는다.
  3. 모든 원시 값과 문자열을 포장한다.
  4. 일급 컬렉션을 쓴다.
  5. 한 줄에 점을 하나만 찍는다.
  6. 줄여 쓰지 않는다 ( 축약 금지 )
  7. 모든 엔티티를 작게 유지한다.
  8. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  9. Getter / Setter / Property를 쓰지 않는다.

 

한 줄에 점을 하나만 찍는다.

 

 

단순히 한 줄에 점의 개수를 헤아려 줄이라는것이 아닙니다. 점을 찍는 행위는 인스턴스의 접근을 하는 행위로 이는 호출자와 피호출자와의 결합도가 강하게 형성된다는 의미입니다.

 

이전시간인 일급 컬렉션에서 사용되었던 List<Student> students를 예시로 사용해보겠습니다.

 

public class Student {

    private Name name;
    private Age age;
    private Score score;
    
    .. Getter
}

Score 객체를 추가로 넣었습니다. 내부에는 int score가 존재합니다.

 

List<Student> 내에서 가장 점수가 높은 학생을 선발한다고 가정한다면

public static void main(String[] args) {
    List<Student> students = new ArrayList<>();
    
    Student student = getTopScoreStudent(students);

}

private Student getTopScoreStudent(List<Student> students) {
    Student topStudent = null;
    for (Student student : students) {
        if (topStudent == null) {
            topStudent = student;
            continue;
        }
        if (topStudent.getScore().getScore() < student.getScore().getScore()) {
            topStudent = student;
        }
    }
    return topStudent;
}

이렇게 작성될것입니다. 하지만 처음에 언급했듯이 이 클래스와 Score와는 아무런 관계가 없습니다 즉, 불필요한 결합도가 높아졌다는 것을 의미합니다. 만약 Student 객체에 대한 변경이 생긴다면? Score 객체에 대한 변경이 생긴다면? Score 의 멤버변수 타입이 변경된다면? 당연하게도 위에 메서드를 사용하고있는 객체에 변경이 불가피합니다. 그리고 변경과정에서 오류의 가능성도 증가하겠죠.

 

그럼 어떻게 해야하는지 차근차근 알아봅시다.

 

 

먼저 저번시간에 만들었던 것처럼 일급클래스를 적용시켜줍니다.

public class StudentList {

    private final List<Student> students;

    public Student getTopScoreStudent() {
        Student topStudent = null;
        for (Student student : students) {
            if (topStudent == null) {
                topStudent = student;
                continue;
            }

            if (topStudent.getScore().getScore() < student.getScore().getScore()) {
                topStudent = student;
            }
        }
        return topStudent;
    }
    
    // 나머지 메서드 ...
}

 

일급 클래스만 적용시켜도 Main 클래스와의 결합도가 낮아졌습니다.

 

 

이제 Student 객체로 일을 넘겨보겠습니다.

public class StudentList {

    private final List<Student> students;

    public Student getTopScoreStudent() {
        Student topStudent = null;
        for (Student student : students) {
            // 생략 ...

            topStudent = topStudent.compareScore(student);
        }
        return topStudent;
    }
    
    // 나머지 메서드 ...
}

public class Student {

    private Name name;
    private Age age;
    private Score score;

    public Student compareScore(Student student) {
        if (score.getScore() < student.getScore().getScore()) {
            return student;
        }
        return this;
    }
}

 

이제 마지막으로 Score 객체에 일을 넘겨보겠습니다.

 

public class Student {

    private Name name;
    private Age age;
    private Score score;

    public Student compareScore(Student student) {
        Score compareScore = score.compareScore(student.getScore());
        if (this.score == compareScore) { // 같은 주소값을 반환하기 때문에 == 를 사용했습니다.
            return this;
        }
        return student;
    }
    
    // 이하 생략 ...
}

public class Score {

    private int score;

    public Score(int score) {
        this.score = score;
    }
    public Score compareScore(Score arguScore) {
        if (score < arguScore.getScore()) {
            return arguScore;
        }
        return this;
    }
    
    // 이하 생략 ...
}

 

기존 코드와 달리 하위 객체의 일을 알 필요가 없어졌고 그저 자신의 일만 하게 되었습니다.

이로써 Main - StudentList - Student - Score 간에 결합도를 완전히 낮췄습니다.

 

결국 이 한 줄에 한 점을 찍는다는 의미는 자신이 알고있는 객체하고만 상호작용하면 되고 제 3자인 객체하고는 상호작용하지 않음으로써 결합도를 약하게 만들어 종속성을 최소화 시키는것입니다.

 

+ Recent posts