객체 지향 설계 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는 문서를 작성하는 기능만 존재합니다.

팩토리 메소드 패턴

팩토리 메소드 패턴은 객체 생성을 Factory 클래스에서 대신 생성하게 하는 디자인 패턴입니다. 즉 객체를 생성할 때 어떤 클래스의 인스턴스를 만들지 서브클래스에서 결정하게 하는겁니다. 이 패턴은 기존의 코드의 변경 없이 확장하기 위한 패턴입니다.

 

MemberFactory는 Member 인터페이스에만 의존하고 실제로 어떤 구현체를 호출할지는 MemberFactory에서 결정하게 됩니다. 이렇게 하면 구현체가 추가되어도 기존 코드를 수정하기 않고 확장 시킬 수 있습니다.

 

 

 

팩토리 메소드 구현

 

1. Member Interface

public interface Member {

    void signup();
}


public class NaverMember implements Member{

    @Override
    public void signup() {
        System.out.println("네이버 가입");
    }
    
}

public class KakaoMember implements Member {

    @Override
    public void signup() {
        System.out.println("카카오 가입");
    }
    
}

 

 

2. MemberFactory

public abstract class MemberFactory {

    public Member newMember() {
        Member member = createMember();
        member.signup();
        return member;
    }
    
    protected abstract Member createMember();
    
}

// Java 8 이후부터 interface에 default 메서드를 통해 구현할 수 있습니다. 
// 따라서 abstract를 사용하지않고 interface를 사용해도 됩니다.
// 아래 Factory 구현체는 interface를 사용했습니다.

public interface MemberFactory {

    default Member newMember() {
        Member member = createMember();
        member.signup();
        return member;
    }
    
    Member createMember();
    
}

public class NaverMemberFactory implements MemberFactory{

    @Override
    public Member createMember() {
        return new NaverMember();
    }
    
}

public class KakaoMemberFactory implements MemberFactory{

    @Override
    public Member createMember() {
        return new KakaoMember();
    }
    
}

 

 

3. Client

public class Main {

    public static void main(String[] args) {

        MemberFactory factory = new NaverMemberFactory();
        Member member = factory.newMember();
        
        // 네이버 가입
        
    }
}

 

 

팩토리 메소드 패턴의 흐름

  1. MemberFactory 구현체인 NaverMemberFactory 객체를 생성
  2. newMember() 메소드내부에서 createMember() 메소드를 실행 -> NaverMemberFactory 클래스에서 오버라이드 한 NaverMember 객체생성
  3. signup() 메소드 실행 -> NaverMember 클래스에서 오버라이드한 signup() 메소드 호출
  4. "네이버 가입" 출력

 

구현체 확장

public class GoogleMember implements Member{

    @Override
    public void signup() {
        System.out.println("구글 가입");
    }
    
}

public class GoogleMemberFactory implements MemberFactory{

    @Override
    public Member createMember() {
        return new GoogleMember();
    }
    
}

Member 인터페이스의 구현체 GoogleMember 클래스를 추가하고 MemberFactory 인터페이스의 구현체 GoogleMemberFactory 클래스를 추가해주는 것으로 쉽게 확장할 수 있습니다.

 

 

 

패턴의 장/단점

장점

  • OCP 원칙 : 기존 코드의 수정 및 변경 없이 확장할 수 있습니다.
  • SRP 원칙 : 하나의 책임을 가지는 클래스로써 코드를 유지보수하기 쉽습니다.

단점

  • 구현체마다 각각의 Factory 클래스를 모두 구현해주어야 하기 때문에 구현체가 늘어날때마다 Factory 클래스도 함께 생성해야합니다. 따라서 관리해야하는 클래스가 증가합니다.
  • 코드의 복잡성이 증가합니다.

 

빌더 (Builder)

빌더 패턴은 복잡한 객체의 생성과 표현을 분리하여 인스턴스를 만드는 생성패턴입니다. 생성자에 들어갈 매개 변수를 메서드로 하나하나 받아들이고 마지막에 통합 빌드해서 객체를 생성하는 방식입니다.

 

 

빌더패턴을 사용하는 이유

우리는 데이터를 주입할 때 생성자를 이용하거나 기본생성자로 객체를 생성하고 이후에 Setter를 이용해서 값을 넣었습니다.

 

 

 

생성자를 이용한 주입

public class User {

    private String name;
    private int age;

    private String sex;
    private String phone;
    private int height;

    public User(String name, int age, String sex, String phone, int height) {
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.phone = phone;
        this.height = height;
    }
}

public class Main {

    public static void main(String[] args) {

        User user = new User("김OO", 20, "남자", "01012345678", 70);

    }
}

 

Setter를 이용한 주입

public class User {

    private String name;
    private int age;

    private String sex;
    private String phone;
    private int height;
    

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public void setHeight(int height) {
        this.height = height;
    }
}

public class Main {

    public static void main(String[] args) {

        User user = new User();
        user.setName("김OO");
        user.setAge(20);
        user.setSex("남자");
        user.setPhone("01012345678");
        user.setHeight(70);
    }
}

 

이러한 방식은 생성자의 인자가 많아질 수록 데이터를 구별하기 어렵고, 필드와 생성자의 위치를 직관적으로 파악하기 어렵다는 것입니다. 그리고 타입이 많아질 수록 메서드 수가 기하급수적으로 늘어나 가독성과 유지보수 측면에서 안좋을 수 밖에 없습니다.

Setter는 객체를 생성하는 시점에 값을 주입하지않아 일관성(Consistency) 불변성(Immutable) 문제가 생길 수 있습니다.

 

생성자와 Setter의 문제점

 

 

일관성 문제

필수 매개변수란 객체가 초기화 될 때 반드시 설정되어야 하는 값입니다. 만약 개발자가 setAge() 메서드를 호출 하지 않고 User 객체를 생성했다면 이 객체는 일관성이 없는 상태가 됩니다. 만약 이 객체를 사용하게 된다면 예외가 발생할 수 있습니다.

 

불변성 문제

Setter의 문제는 객체를 생성한 이후에 값을 주입하는데요 이는 객체를 생성한 이후로도 누구나 외부에서 Setter를 사용해서 값을 주입할 수 있는 문제가 생깁니다. 다시 말해 누구든지 Setter를 호출해 객체를 조작할 수 있다는 뜻입니다. 따라서 Setter는 불변성이 보장되지않는 객체라고 할 수 있습니다.

 

 

 

Builder패턴의 사용

 

빌더 패턴은 이러한 문제들을 해결하기위해 Buillder 클래스를 만들어 생성되는 시점에 모든 값들을 넣어주도록 하는 패턴입니다.

 

        // 생성자 주입
        User user1 = new User("김OO", 20, "남자", "01012345678", 70);

        // Setter 주입
        User user2 = new User();
        user.setName("김OO");
        user.setAge(20);
        user.setSex("남자");
        user.setPhone("01012345678");
        user.setHeight(70);

        // Builder 주입
        User user3 = User.builder()
            .name("김OO")
            .age(20)
            .sex("남자")
            .phone("01012345678")
            .height(70)
            .build();

 

빌더 패턴을 이용하면 생성자를 오버로딩을 하지 않아도되며, 데이터의 순서에 상관없이 데이터를 주입할 수 있게되었습니다.

 

 

 

빌더패턴의 장점

  • 필요한 데이터만 설정할 수 있습니다.
  • 가독성이 좋습니다.
  • 변경 가능성을 최소화 할 수 있습니다.

 

 

빌더패턴의 구조

구현자체는 어렵지 않습니다. 위의 User 객체에 Builder 패턴을 구현해보겠습니다.

 


1. builder()

User 클래스 안에 Builder 클래스를 구현해줍니다.

public class User {

    private String name;
    private int age;

    private String sex;
    private String phone;
    private int height;

    public static UserBuilder builder() {
        return new UserBuilder();
    }

    public static class UserBuilder {

    }

}

 

 

User.builder()로 Builder 패턴을 시작해야하기 때문에 static 을 사용해 클래스를 만들어줍니다.

 

 

 

2. Chaning

    public static class UserBuilder {

        private final User user;

        public UserBuilder() {
            this.user = new User();
        }

        public UserBuilder name(String name) {
            user.name = name;
            return this;
        }

        public UserBuilder age(int age) {
            user.age = age;
            return this;
        }

        public UserBuilder sex(String sex) {
            user.sex = sex;
            return this;
        }

        public UserBuilder phone(String phone) {
            user.phone = phone;
            return this;
        }

        public UserBuilder height(int height) {
            user.height = height;
            return this;
        }

        public User build() {
            return user;
        }
    }

 

User.builder() 를 입력하면 UserBuilder 기본생성자를 통해 User 기본생성자를 주입시켜줍니다. 여기서 주의할 사항은 User의 기본생성자가 존재해야합니다. 인자를 받는 생성자가 존재한다면 기본생성자를 하나 더 만들어주어야합니다.

 

이후에 반환타입은 UserBuilder, 메서드명은 User의 변수명으로 만들어 메서드를 체이닝(Chaining) 형태로 호출하고 마지막에 build()를 통해 최종적으로 객체를 반환하도록 만들어주면 Builder 패턴을 쉽게 구현할 수 있습니다.

GET과 POST의 차이

 

 

HTTP Method

HTTP Method는 클라이언트가 서버에 요청의 목적 및 종류를 알리는 수단입니다.

그 종류에는 GET, POST, PUT ,PATCH, DELETE.. 등 여러가지가 있습니다. 오늘은 가장 많이 쓰이는 GET과 POST에 대해서 알아보겠습니다.

 


GET

GET 방식은 주로 서버에서 리소스를 조회할때 사용됩니다.

 

특징

  • 캐시가 가능하다
    • 리소스를 요청할 때 웹 캐시가 요청을 가로채 리소스 복사본을 반환합니다. HTTP Header에서 cache-control 헤더를 통해 캐시 옵션을 지정할 수 있습니다.
  • 브라우저 히스토리에 남습니다.
  • GET은 SELECT 성향이 있어 서버에서 데이터 조회하는 용도로 활용됩니다.
  • URL에 데이터를 포함하여 요청할 수 있습니다. (쿼리스트링)
  • 데이터를 Header에 포함하여 전송합니다.

 

쿼리 스트링 또는 쿼리 파라미터

 

쿼리스트링은 ?로 시작하여 키(Key) = 값(Value) 형태로 전송할 수 있고 & 기호를 사용하여 다수의 데이터를 전송 할 수 있습니다. 다만 문제는 Header에 데이터가 노출되는 문제가 발생합니다. 


POST

POST 방식은 서버에 리소스를 처리할 때 사용됩니다. 주로 데이터를 추가하거나 수정하기 위해 사용됩니다.

 

특징

  • 캐시가 불가능 합니다.
  • 브라우저 히스토리에 남지 않습니다.
  • URL에 데이터를 노출하지 않고 요청할 수 있습니다.
  • 데이터를 Body에 포함하여 전송합니다.

GET과 POST의 차이

  • 사용목적
    • GET : 데이터 조회
    • POST : 데이터 생성 또는 수정
  • 데이터 위치
    • GET : Header
    • POST : Body
  • 멱등성(Idempotent) : 연산을 여러 번 하더라도 결과가 달라지지 않는 성질을 의미합니다.
    • GET : 멱등성 보장
    • POST : 멱등성 보장 X 

https://ko.wikipedia.org/wiki/HTTP

 

추상 팩토리 패턴

서로 관련이 있는 객체들을 통째로 묶어서 팩토리 클래스로 만들고,

이들 팩토리를 조건에 따라 생성하도록 다시 팩토리를 만들어서 객체를 생성하는 패턴입니다.

 

쉽게말해 추상적인 공장에서는 추상적인 부품을 조합하여 추상적인 제품을 만든다고 생각하시면됩니다.

무슨말이야? 라고 하신다면 '추상화' 라는 말은 '구체적으로 어떻게 구현되어 있는지 생각하지 않고 인터페이스에만 주목하는 상태'를 뜻합니다.

 

추상화에 대표적인 예로 부품이 많은 자동차, 컴퓨터 등이 있으나 저는 이번에 컴퓨터로 예를 들어보겠습니다.

컴퓨터의 부품으로는 키보드, 마우스, 모니터, 스피커 등등 많습니다.

각 부품마다 제조사도 다르고 일부 지원하는 기능도 다양합니다. 우리가 원래 사용하던 부품이 아니고 다른 제조사의 부품을 갈아낀다고해서 컴퓨터가 작동이 안될까요? 당연히 아닙니다. 왜냐하면 각 부품마다 인터페이스화 되어있기때문입니다. 일종의 가이드라인 또는 설계도면인것인 셈이죠.

 

이를 코드로 작성한다면 키보드, 마우스, 모니터, 스피커는 interface 입니다. 그리고 제조사별로 구현체를 갈아끼울 수 있겠죠.

 

이런식으로 말입니다. 여기에서 Factory는 어떤 구현체를 적용할 지 조합하는 공장입니다.

 

지금부터 코드를 작성하며 같이 보겠습니다. 예시를 위해 키보드와 마우스에 대해서만 작성해보도록 하겠습니다.

 

 

추상팩토리패턴의 예

 

1. 키보드

public interface Keyboard {

    void init();

}

public class LGKeyboard implements Keyboard{

    @Override
    public void init() {
        System.out.println("LG Keyboard Call");
    }
}

public class SAMSUNGKeyboard implements Keyboard{

    @Override
    public void init() {
        System.out.println("SAMSUNG Keyboard Call");
    }
}

 

 

 

 

2. 마우스

public interface Mouse {

    void init();
    
    void sound();
   
}

public class LGMouse implements Mouse{

    @Override
    public void init() {
        System.out.println("LG Mouse Call");
    }

    @Override
    public void sound() {
        System.out.println("LG Sound");
    }
}

public class SANSUNGMouse implements Mouse{

    @Override
    public void init() {
        System.out.println("SAMSUNG Mouse Call");
    }

    @Override
    public void sound() {
        System.out.println("SAMSUNG Sound");
    }
}

 

 

인터페이스에 대한 구현체는 모두 만들었습니다. 다음은 Factory를 만들어보겠습니다.

 

 

 

 

3. 팩토리

public class KeyboardFactory {

    public Keyboard createKeyboard(String type) {
        if ("LG".equals(type)) {
            return new LGKeyboard();
        }
        if ("SAMSUNG".equals(type)) {
            return new SAMSUNGKeyboard();
        }
        return new Keyboard() {
            @Override
            public void init() {
                System.out.println("Default Keyboard Call");
            }
        };
    }
}

public class MouseFactory {

    public Mouse createMouse(String type) {
        if ("LG".equals(type)) {
            return new LGMouse();
        }
        if ("SAMSUNG".equals(type)) {
            return new SANSUNGMouse();
        }
        return new Mouse() {
            @Override
            public void init() {
                System.out.println("Default Mouse Call");
            }

            @Override
            public void sound() {
                System.out.println("Default Sound");
            }
        };
    }

}

 

Factory 클래스에서 만약 type이 LG라면 LG에 맞는 구현체를 넣고, Samsung이라면 Samsung에 맞는 구현체를 넣어주는 부분입니다. 만약 둘다 아니라면 익명클래스를 이용해서 기본 부품을 넣어주었습니다.

 

이제 컴퓨터를 조립해볼까요?

 

 

 

4. 완성

public class Computer {

    private Keyboard keyboard;
    private Mouse mouse;

    public Computer(String type) {
        keyboard = createKeyboard(type);
        mouse = createMouse(type);
    }

    public void call() {
        keyboard.init();
        mouse.init();
    }

    public void click() {
        mouse.sound();
    }

    private Keyboard createKeyboard(String type) {
        KeyboardFactory keyboardFactory = new KeyboardFactory();
        return keyboardFactory.createKeyboard(type);
    }

    private Mouse createMouse(String type) {
        MouseFactory mouseFactory = new MouseFactory();
        return mouseFactory.createMouse(type);
    }

}

 

Factory 클래스의 createKeyboard와 createMouse 메서드를 사용해서 구현체를 넣어주었습니다.

 

실제 컴퓨터를 사용해보겠습니다.

 

추상팩토리패턴의 결과

public class Main {

    public static void main(String[] args) {

        Computer computer = new Computer("LG");
        computer.call();

        computer.click();
        
        // LG Keyboard Call
        // LG Mouse Call
        // LG Sound
        
        Computer computer2 = new Computer("APPLE");
        computer2.call();

        computer2.click();

        // Default Keyboard Call
        // Default Mouse Call
        // Default Sound

    }
}

 

 

이 추상 팩토리 패턴의 장점은  공장에서 생성되는 제품들의 상호 호환을 보장할 수 있습니다.

그리고 구현체들은 한 곳에서 코드를 쉽게 유지보수할 수 있습니다.

 

단점으로는 패턴과 함께 새로운 인터페이스들과 클래스들이 많이 도입되기 때문에 코드가 필요 이상으로 복잡해질 수 있습니다.

객체지향 생활체조 원칙은 소트웍스 앤솔러지(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줄 줄어들었습니다. 강박적으로 줄이라면 더욱 줄일 수 있겠지만 이 정도만해도 최대한 엔티티를 작게 유지하도록 노력한것이라고 봐도 무방하겠죠?

 

+ Recent posts