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

목차

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

 

모든 원시 값과 문자열을 포장한다.

 

쉽게말해 int, String 과 같은 타입의 값을 객체로 포장하는겁니다.

 

public class Student {

    private String name;
    private int age;

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

 

Student 객체에는 이름과 나이를 가지고 있습니다.

 

만약 이름은 최소 2자 이상, 나이는 20세 이상 이라는 조건이 추가된다면?

 

public class Student {

    private String name;
    private int age;

    public Student(String name, int age) {
        validName(name);
        validAge(age);
        this.name = name;
        this.age = age;
    }

    private void validName(String name) {
        if (name.length() < 2) {
            throw new IllegalStudentNameException("이름은 최소 2자 이상이어야 합니다.");
        }
    }
    private void validAge(int age) {
        if (age < 20) {
            throw new IllegalStudentAgeException("나이는 20살 이상이어야 합니다.");
        }
    }

}

 

잘못된 값의 검증을 제외한 일반적인 예외처리를 했음에도 Student가 할 일이 엄청 늘어났습니다.

Student는 이제 이름과 나이에 대한 상태관리를 모두 해야합니다.

그럼 만약 Student에 이메일, 성별 등 변수가 추가된다면 코드는 복잡해질것입니다.

 

이제 원시 타입 변수로 포장해보겠습니다.

public class Student {

    private Name name;
    private Age age;

    public Student(Name name, Age age) {
        this.name = name;
        this.age = age;
    }
}

public class Name {

    private String name;

    public Name(String name) {
        validName(name);
        this.name = name;
    }

    private void validName(String name) {
        if (name.length() < 2) {
            throw new IllegalStudentNameException("이름은 최소 2자 이상이어야 합니다.");
        }
    }

}

public class Age {

    private int age;

    public Age(int age) {
        validAge(age);
        this.age = age;
    }

    private void validAge(int age) {
        if (age < 20) {
            throw new IllegalStudentAgeException("나이는 20살 이상이어야 합니다.");
        }
    }

}

 

이제 Student에서 이름과 나이에 대한 책임이 분리 되면서 단일 책임 원칙(SRP)도 지키게 되었습니다.

제네릭이란?

이 글을 보러 오신분들은 대부분 자바를 공부하면서 제네릭이 뭔지 궁금해서 들어온 분들이실겁니다. 

제네릭(generic)이란 데이터의 타입을 일반화한다(generalize)는 것을 의미합니다.

이렇게 적으면 아마 이해가 어려우실겁니다.

 

 

사실 우리가 흔히 사용하는 List, Map, Set 등의 컬렉션에서 자주 볼 수 있습니다.

 

public static void main(String[] args) {

        List<String> stringList = new ArrayList<>();
        List<Integer> integerList = new ArrayList<>();
        Map<String, Integer> map = new HashMap<>();
        Set<Double> set = new HashSet<>();
        
}

 

객체<타입> 객체명 = new 객체<타입>();

 

이 형식에서 < > 이것이 바로 제네릭 입니다.

 

즉 제네릭은 클래스 외부에서 사용자에 의해 타입을 지정되는 것 ( 타입을 일반화한다 ) 을 의미합니다.

 

 

제네릭의 특징

  1. 클래스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높일 수 있다.
  2. 반환값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄일 수 있다.
  3. 잘못된 타입이 입력되는 것을 컴파일 단계에서 방지할 수 있다.
  4. 재사용성이 높다.

 

제네릭 사용방법

타입 설명
<T> Type
<E> Element
<K> Key
<V> Value

이 표현식은 관례적으로 사용되고 있습니다. 반드시 일치해야할 필요도 없고 한 글자일 필요도 없습니다.

하지만 개발은 혼자하는것이 아니기에...

 

 

public interface Generic <T> { ... }
public class Generic <T> { ... }
public class Generic <T, K> { ... }

클래스와 인터페이스는 이렇게 선언하면 됩니다.

 

이번에는 List와 같은 클래스를 하나 만들어 보겠습니다.

 

제네릭 클래스

public class Generic <T> {

    private final List<T> list = new ArrayList<>();


    public void add(T value) {
        list.add(value);
    }

    public void remove(T value) {
        list.remove(value);
    }

    public T get(int index) {
        return list.get(index);
    }

}

public static void main(String[] args) {

    Generic<String> generic = new Generic<>();

    String test = "제네릭";
    generic.add(test);
    generic.remove(test);

    int size = generic.size();
    System.out.println(size); // 0
}

 

만약 String 이 아닌 타입이 입력된다면?

public static void main(String[] args) {

    Generic<String> generic = new Generic<>();

    int test = 1;
    generic.add(test); // 컴파일 에러!
    generic.remove(test); // 컴파일 에러!

    int size = generic.size();
    System.out.println(size);
}

컴파일 과정에서 오류를 방지할 수 있습니다.

 

제네릭 메서드

// [접근제어자] <제네릭타입> [반환타입] [변수명] ( [제네릭타입] 매개변수명 )
public <T> T getMethod(T value) { ... }

 

 

 

 

 

와일드 카드 < ? >

위에서의 제네릭에서는 타입을 제한하지 않았습니다. 와일드카드는 타입의 제한을 둘 수 있습니다.

<K extends T> { ... } // T, T의 자식 타입만 가능
<K super T> { ... } // T, T의 부모 타입만 가능

<? extends T> { ... } // T, T의 자식 타입만 가능
<? super T> { ... } // T, T의 부모 타입만 가능

 

여기서 K extends T와 ? extends T 는 차이가 있습니다.

K는 타입이 지정이 되지만 ?는 타입이 지정되지 않습니다.

 

여기서 주의점은 와일드카드는 클래스에 사용할 수 없다는 것입니다. 클래스에서는 타입을  <K extends T>와 같이 지정해주어야합니다.

 

public class Parent{
    
    public void call() {
        System.out.println("Parent Call");
    }
}

public class Children extends Parent{

    @Override
    public void call() {
        System.out.println("Children Call");
    }
}

public class Main {

    public static void main(String[] args) {
        List<Parent> list = new ArrayList<>();

        list.add(new Parent());
        list.add(new Children());

        listCall(list);
    }

    private static void listCall(List<? extends Parent> list) {
        for (Parent parent : list) {
            parent.call();
        }
        // Parent Call 출력
        // Children Call 출력
    }
}

 

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

목차

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

 

 

 

else 예약어를 사용하지 않는다.

 

if-else에서 else 예약어를 사용하지 않는다는 의미입니다. switch문도 허용하지 않습니다.

 

public class ex2 {

    public static void main(String[] args) {

        String select = "1";

        String payment = getPayment(select);
        System.out.println(payment); // "신용카드"
    }

    private static String getPayment(String select) {
        String payment = null;

        if ("1".equals(select)) {
            payment = "신용카드";
        } else if ("2".equals(select)) {
            payment = "무통장입금";
        } else if ("3".equals(select)) {
            payment = "카카오페이";
        } else {
            payment = "네이버페이";
        }
        return payment;
    }
}

 

if-else 문을 사용하여 코드를 작성했습니다. 겉보기에는 가독성도 괜찮고 전혀 문제될것이 없는 코드 같습니다. 하지만 실제 로직을 작성한다면 이보다 훨씬 복잡한 코드를 작성하게 될것입니다.

 

이 코드의 비즈니스적으로 문제가 있습니다. 사용자가 1,2,3,4가 아닌 다른 값을 넣었다면? 개발자는 예외가 발생하길 원하지만 실제로는 "네이버페이"가 정상적으로 반환될것입니다. 이는 설계의도를 완전히 벗어난 결과입니다. 이제 early return 구조를 적용해보겠습니다.

 

public class ex2 {

    public static void main(String[] args) {

        String select = "5";

        String payment = getPayment(select); // NotFoundPaymentException 예외발생!!
        System.out.println(payment);
    }

    private static String getPayment(String select) {

        if ("1".equals(select)) {
            return "카드";
        }
        if ("2".equals(select)) {
            return "무통장입금";
        }
        if ("3".equals(select)) {
            return "카카오페이";
        }
        if ("4".equals(select)){
            return "네이버 페이";
        }
        throw new NotFoundPaymentException("잘못된 결제방식입니다.");
    }
}

 

구조를 살짝 바꾸면서 메서드의 가독성이 높아졌습니다. 개발자가 의도한대로 조건에 만족한다면 결제방식을 바로 반환하며 종료됩니다. 반대로 조건에 만족하는 결과가 없다면 예외를 발생시키면서 설계의도대로 동작할것입니다. 

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

목차

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

 

 

한 메서드에 오직 한 단계의 들여쓰기만 한다.

 

들여쓰기의 깊이를 2 이상 두지 말라는 지침입니다. for 문안에 if문이 있으면 깊이가 2인 코드가 됩니다. 따라서 한 메서드 안에 깊이가 2 이상인 코드가 존재하면 안된다는 말과 같습니다.

 

이 원칙의 의미는 코드를 작성할때 무조건 들여쓰기를 지양하라는 것보다는 메서드를 분리함으로써 하나의 메서드가 하나의 일을 하도록 설계하자는데 의의가 있습니다. 일을 잘개 쪼갤수록 결합도는 낮아지고, 응집도는 높아집니다. 이러한 코드는 재사용성이 높고, 디버깅에 용이해집니다. 따라서 한 메서드에는 한가지의 주요 작업을 수행하도록 구성하는 것이 좋습니다.

 

public class ex1 {

    static List<Integer> boardNumbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);

    public static void main(String[] args) {
        int sum = sumNumbers();
        System.out.println(sum);
    }

    private static int sumNumbers() {
        int sum = 0;
        for (int number : boardNumbers) {
            if (number > 5) {
                sum += number;
            }
        }
        return sum;
    }
}

 

boardNumbers에서 5 이상인 수만 더해서 반환하는 메서드 입니다. for 문 안에 if문이 있다는건 깊이가 2라는 의미입니다.

메서드를 분리해보겠습니다.

 

public class ex1 {

    static List<Integer> boardNumbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);

    public static void main(String[] args) {
        int sum = sumNumbers();
        System.out.println(sum);
    }

    private static int sumNumbers() {
        int sum = 0;
        for (int number : boardNumbers) {
            sum += check(number);
        }
        return sum;
    }

    private static int check(int number) {
        if (number > 5) {
            return number;
        }
        return 0;
    }
}

 

메서드를 분리함으로써 sumNumbers()는 수를 더하는 일만 하게되었고, check(int number)는 수의 조건을 확인하는 일만 하게되었습니다. 예시코드가 짧아서 들여쓰기에 대한 장점이 잘 부각되지않은것 같지만 하나의 메서드는 하나의 일을 한다는 개념을 이해하셨다면 충분합니다.

 

 

 

 

 

 

+ Recent posts