프로토타입 패턴

프로토타입(Prototype) 패턴은 객체를 생성하는 비용이 높은 경우, 기존 객체를 복제하여 새로운 객체를 생성하는 패턴입니다.

다시 말해 기존의 객체를 새로운 객체로 복사하여 우리의 필요에 따라 수정하는 메커니즘을 제공합니다.

 

이 프로토타입 패턴은 객체를 생성하는 데 비용이 많이 들고, 이미 유사한 객체가 존재하는 경우에 사용됩니다. DB로부터 가져온 데이터를 수차례 수정해야하는 경우 new 라는 키워드를 통해 객체를 생성하는 것은 비용이 너무 많이 들기 때문에 한 번 DB에 접근하여 객체를 가져오고 필요에 따라 새로운 객체에 복사하여 데이터를 수정하는 것이 더 좋은 방법입니다.

 

 

 

 

 

 

 

 

프로토타입 패턴 구현

Java에서는 이 패턴을 구현하기 위해 Cloneable 인터페이스를 사용합니다. Cloneable 인터페이스는 마커(marker) 인터페이스로, 별도의 메소드가 정의되어 있지 않습니다. 이 인터페이스를 구현하는 클래스는 얕은 복사 (shallow copy) 를 지원한다는 표시입니다.

 

Cloneable 인터페이스를 구현하는 이유는 clone() 메소드를 호출하여 객체를 복제하기 위함입니다. clone() 메소드는 Object 클래스에서 상속받은 메소드 중 하나로, 복제 가능한 객체에서 호출할 수 있습니다. 그러나 Cloneable을 구현하지 않은 객체가 clone()을 호출하게 되면, CloneNotSupportedException이 발생합니다.

 

public class Cookie{

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {

        Cookie cookie = new Cookie();
        cookie.clone(); // CloneNotSupportedException 발생!

    }
}

 

따라서 Cloneable 인터페이스를 구현함으로써 해당 클래스의 인스턴스가 복제 가능하다는 것을 나타내고, 이를 통해 프로토타입 패턴을 적용할 수 있게 됩니다. 하지만 주의할 점은 clone() 메소드는 얕은 복사를 수행하므로, 필요에 따라서 깊은 복사(Deep Copy)를 구현해야 할 수 있습니다.

 

 

 

얕은 복사

얕은 복사 (Shallow Copy)는 객체의 필드들을 복제할 때, 참조 타입의 필드는 원본 객체와 복제된 객체가 같은 인스턴스를 참조합니다.

 

public class ShallowCopyExample implements Cloneable {
    private int value;
    private int[] array;

    public ShallowCopyExample(int value, int[] array) {
        this.value = value;
        this.array = array;
    }

    @Override
    public ShallowCopyExample clone() throws CloneNotSupportedException {
        return (ShallowCopyExample) super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        int[] originalArray = {1, 2, 3};
        ShallowCopyExample original = new ShallowCopyExample(42, originalArray);

        ShallowCopyExample cloned = original.clone();

        // 값과 배열은 복제되지만 배열은 같은 배열을 참조하고 있음
        System.out.println(original == cloned); // false
        System.out.println(original.array == cloned.array); // true (얕은 복사)
    }
}

 

 

깊은 복사

깊은 복사 (Deep Copy) 는 참조 타입의 필드도 새로운 인스턴스를 생성하여 복사하게 됩니다. 즉 모든 필드 변수들이 새로운 인스턴스를 가집니다.

 

public class DeepCopyExample implements Cloneable {
    private int value;
    private int[] array;

    public DeepCopyExample(int value, int[] array) {
        this.value = value;
        this.array = array.clone(); // 깊은 복사: 배열의 복사본을 생성하여 참조
    }

    @Override
    public DeepCopyExample clone() throws CloneNotSupportedException {
        return (DeepCopyExample) super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        int[] originalArray = {1, 2, 3};
        DeepCopyExample original = new DeepCopyExample(42, originalArray);

        DeepCopyExample cloned = original.clone();

        // 값은 복제되고 배열은 복제본을 참조하고 있음
        System.out.println(original == cloned); // false
        System.out.println(original.array == cloned.array); // false (깊은 복사)
    }
}

 

 

 

프로토타입 패턴 구현 예제

public class Employee implements Cloneable {

    private String name;
    private int age;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    protected Employee clone() {
        try {
            return (Employee) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

public class Employees {

    private final List<Employee> employeeList;

    public Employees(List<Employee> employeeList) {
        this.employeeList = employeeList;
    }

    public List<Employee> getCloneList() {
        List<Employee> clone = new ArrayList<>();

        for (Employee employee : employeeList) {
            Employee cloneEmployee = employee.clone();
            clone.add(cloneEmployee);
        }

        return clone;
    }
    
    public List<Employee> getOriginalList() {
        return employeeList;
    }
}

 

Cloneable를 사용하는 Employee 에 clone() 메소드를 오버라이드 했고, 그 리스트를 담은 Employees 에 비교를 위해 cloneList와 originalList 메소드를 구현했습니다.

 

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {

        Employee emp1 = new Employee("김OO", 20);
        Employee emp2 = new Employee("이OO", 22);
        Employee emp3 = new Employee("박OO", 25);
        Employee emp4 = new Employee("오OO", 29);
        Employee emp5 = new Employee("나OO", 23);

        List<Employee> employeeList = List.of(emp1, emp2, emp3, emp4, emp5);

        Employees employees = new Employees(employeeList);

        List<Employee> originalList = employees.getOriginalList(); // 같은 인스턴스
        List<Employee> cloneList = employees.getCloneList(); // clone 인스턴스

        System.out.println(employeeList == originalList); // true
        System.out.println(employeeList == cloneList); // false

        Employee originalEmp = employeeList.get(0); // 같은 인스턴스의 첫번째 Employee
        Employee cloneEmp = cloneList.get(0); // clone 인스턴스의 첫번째 Employee

        System.out.println(originalEmp == cloneEmp); // false

    }
}

 

 

프로토타입 패턴의 장/단점

프로토타입 패턴은 객체를 복제하여 새로운 객체를 생성하는 패턴으로, 이를 통해 객체 생성의 비용을 줄일 수 있습니다. 하지만 프로토타입 패턴은 특정 상황에서 유용하게 사용될 수 있지만, 모든 상황에서 적합하지는 않습니다. 

 

 

장점

  1. 객체 생성 비용 감소 : 객체를 복제함으로써 새로운 객체를 생성할 때의 비용을 줄일 수 있습니다. 특히 복잡한 객체 구조이거나 생성에 많은 리소스가 필요한 경우에 유용합니다.
  2. 동적 객체 생성 : 런타임에서 동적으로 객체를 생성할 수 있습니다. 사용자가 필요에 따라 객체를 생성하고 조작할 수 있습니다.
  3. 객체 생성 시간 단축 : 객체를 복제하여 생성하기 때문에 클래스의 초기화나 설정 작업을 생략할 수 있어 객체 생성 시간을 단축할 수 있습니다.

 

 

단점

  1. Cloneable 인터페이스의 한계 : 프로토타입 패턴을 구현하기 위해서는 Cloneable 인터페이스를 필연적으로 사용하게 되는데 이 인터페이스는 마커(marker) 인터페이스이고, clone() 메소드는 얕은 복사만을 수행하기 때문에 깊은 복사를 수행하려면 추가적인 작업이 필요합니다.
  2. 복제과정의 복잡성 : 객체가 복잡한 구조를 가지거나 참조하는 객체들이 많다면, 이를 적절하게 복제하려면 복잡한 복제 과정이 필요할 수 있습니다. 특히 객체 그래프가 순환이 발생하는 경우에 복제 과정이 더욱 복잡해질 수 있습니다.
  3. 메모리 사용량 증가 : 객체를 복제하여 생성하면 메모리 사용량이 증가할 수 있습니다. 특히 객체가 큰 데이터를 가지고 있거나 복제 해야 할 객체 수가 많을 수록 사용량이 증가합니다.

 

 

프로토타입 패턴의 사용여부는 특정 상황과 요구사항에 따라 주의 깊게 고려하여 사용해야합니다.

객체 지향 설계 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);
    }
    
    // 이하 생략 ...
}

 

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

 

 

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

+ Recent posts