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

목차

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

 

 

일급 컬렉션이란?

Collection을 Wrapping하면서 그 외의 다른 멤버 변수가 없는 상태를 일급 컬렉션이라고 합니다.

일급컬렉션의 장점은 아래와 같습니다.

  1. 비즈니스의 종속적인 자료구조
  2. 불변성 보장
  3. 상태와 행위를 한 곳에서 관리
  4. 이름이 있는 컬렉션

 

@Getter
public class Student {

    private Name name;
    private Age age;

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

 

이전 원시값 포장에 대해서 설명할때 사용되었던 Student 객체입니다.

 

이번에는 학생명단을 만들어보겠습니다.

  • 학생명단은 최대 10명
  • 이름은 중복되지 않아야함

예시를 위해서 두가지의 검증로직이 필요하다고 가정하겠습니다. 그리고 이 검증로직을 Service 계층에서 진행했습니다.

 

public class StudentService {

    public void addStudent(List<Student> students, Student student) {
        validateStudentsSize(students);
        validateStudentName(students, student);

        students.add(student);
    }

    private void validateStudentName(List<Student> students, 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(List<Student> students) {
        if (students.size() > 10) {
            throw new IllegalStudentsException("최대 학생 수는 10명입니다.");
        }
    }
}

 

비즈니스로직을 모두 처리했지만 약간의 문제가 있습니다.

  1. List 컬렉션에 의존적입니다.
  2. List<Student>와 StudentService간에 결합도가 높습니다.
  3. 다른 사람이 보았을 때 List<Student>의 일을 파악하기 어렵습니다.
  4. 컬력션이 변경된다면 OCP원칙에 위배됩니다.

이러한 직접적인 의존성은 프로그램이 변화하거나 성장할 때 유연성을 제한하며, List<Student>의 변경은 StudentService 클래스의 범위를 넓혀 유지보수를 어렵게 만듭니다. 이런 구조는 코드 변경에 취약하며, 새로운 데이터 구조나 변경 사항이 발생할 때마다 Service 로직을 수정해야 하는 번거로움이 발생할 수 있습니다. 또한 이 문제는 개방-폐쇄원칙(OCP 원칙)을 위배하게 됩니다.

 

이제 이 문제들을 해결해 보겠습니다.

 

 

 

1. 비즈니스의 종속적인 자료구조

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);
    }
    
    public List<Student> getStudents() {
        return Collections.unmodifiableList(students);
    }

    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명입니다.");
        }
    }
}

 

StudentList 클래스를 도입함으로써 List<Student> 객체를 캡슐화 했고, Service에서 관리하던 비즈니스로직을 StudentList 클래스로 종속시켰습니다이 변경으로 인해 StudentService(외부에서)는 더 이상 직접적으로 List<Student>를 다루지 않고, StudentList의 메소드를 통해 학생 목록을 조작할 수 있게 되었고, 

 

따라서 컬렉션 변경에 대한 OCP원칙을 지킬 수 있게 되었고, 학생명단과 StudentService간에 결합도가 낮아졌습니다.

 

 

 

2. 불변성 보장

일급 컬렉션은 컬렉션의 불변을 보장하는데, 단순히 final 을 사용하는 것이 아니라 캡슐화를 통해 이뤄집니다.

* final은 재할당만 금지하는 것이므로 add, remove 등이 가능합니다.

 

List<Student> 객체는 StudentList 클래스 내부에 있기때문에 Student 하위에 setter가 존재하지 않는 한 값의 변경이 일어날 수 없습니다. 불변성에 대한 자세한 내용은 차후 다룰 예정입니다.

 

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

 

// getStudents : Collections.unmodifiableList(students);
List<Student> students = studentList.getStudents();
Student newStudent = new Student(new Name("이OO"), new Age(21));
students.add(newStudent); // UnsupportedOperationException 런타임 예외발생!

 

 

3. 상태와 행위를 한 곳에서 관리

일급컬렉션은 값과 로직이 한 객체안에 있기때문에 응집도가 높아집니다. 따라서 변경이 용이하고 유지보수가 수월해집니다.

 

만약 비즈니스 로직이 추가된다면?

Service에서 로직을 추가할 경우 결합도만 높아지게 되고 수정사항이 증가하게 될겁니다.

하지만 일급컬렉션을 사용한다면 클래스 내부에서 수정 및 추가만 하면 됩니다.

 

public class StudentList {

    private final List<Student> students;

    public StudentList() {
        this.students = new ArrayList<>();
    }
	
    // 만약 나이의 평균을 구하는 로직이 필요하다면? 
    public double averageAge() {
        return students.stream()
            .mapToInt(student -> student.getAge().getAge())
            .average()
            .orElse(0);
    }
	
    // 나머지 메서드 ...

}

 

 

4. 이름이 있는 컬렉션

List<Student> 만으로 그 의미를 추론하기는 어렵습니다. 하지만 StudentList 또는 그 상황에 맞는 이름을 붙혀준다면 그 의미는 명확해지고, 다른 사람이 코드를 분석할 때 객체 이름으로 어떤일을 할지 파악하기 쉬워진다는 겁니다.

 

public static void main(String[] args) {

    // 같은 List<Payment> 이기 때문에 ,
    // List<Payment>를 매개변수로 받는 메서드에는 모두 들어갈 수 있습니다.
    List<Payment> kakaoPay = createPayments();
    List<Payment> naverPay = createPayments();

    // 객체가 다르다면 사용용도가 명확해집니다.
    KakaoPay kakaoPay = createKakaoPay();
    NaverPay naverPay = createNaverPay();

}

 

 

객체지향 생활체조 원칙은 소트웍스 앤솔러지(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)는 수의 조건을 확인하는 일만 하게되었습니다. 예시코드가 짧아서 들여쓰기에 대한 장점이 잘 부각되지않은것 같지만 하나의 메서드는 하나의 일을 한다는 개념을 이해하셨다면 충분합니다.

 

 

 

 

 

 

버블정렬

https://ko.wikipedia.org/wiki/버블_정렬

특징

  • 안정정렬 (stable sort) 이다 
  • 추가적인 공간이 필요하지 않은 제자리 정렬( in-place sort ) 이다

 

시간복잡도

  시간복잡도
Best O(N)
Average O(N2)
Worst O(N2)

장점

  1. 구현이 매우 쉽다.
  2. 추가적인 메모리 소비가 적다.
  3. 안정정렬이다.

 

단점

  1. 정렬하는데 오랜시간이 걸린다. (시간복잡도가 높다)

 

 

 

 

정렬방법

  1. 현재 원소다음 원소를 비교한다.
  2. 현재 원소 > 다음 원소 이면 둘의 위치를 바꾼다.
  3. 다음 원소로 이동한다.

 

유튜브 영상으로도 버블정렬을 확인할 수 있습니다.

https://www.youtube.com/watch?v=Cq7SMsQBEUw

 

자바로 구현

 

기본 버블정렬

public class Bubble_Sort {

    public static void main(String[] args) {
        int[] array = {5, 3, 1, 4, 2};
        System.out.printf("초기배열 : {%d, %d, %d, %d, %d} \n", array[0], array[1], array[2], array[3], array[4]);
        bubble_sort(array);
        System.out.printf("정렬된배열 : {%d, %d, %d, %d, %d} \n", array[0], array[1], array[2], array[3], array[4]);
    }

    private static void bubble_sort(int[] array) {
        // i 는 위 그림의 Round와 같음
        for (int i = 1; i < array.length; i++) {

            // Round별 비교횟수는 Round가 증가함에 따라 횟수가 줄어듬
            for (int j = 0; j < array.length - i; j++) {

                // 현재원소가 다음원소보다 크다면 두개의 값을 변경한다.
                if (array[j] > array[j + 1]) {
                    swap(array, j);
                }
            }
        }
    }

    private static void swap(int[] array, int index) {
        int temp = array[index];
        array[index] = array[index + 1];
        array[index + 1] = temp;
    }
}

 

최적화된 버블정렬

public class Bubble_Sort {

    public static void main(String[] args) {
        int[] array = {5, 3, 1, 4, 2};
        System.out.printf("초기배열 : {%d, %d, %d, %d, %d} \n", array[0], array[1], array[2], array[3], array[4]);
        bubble_sort(array);
        System.out.printf("정렬된배열 : {%d, %d, %d, %d, %d} \n", array[0], array[1], array[2], array[3], array[4]);
    }

    private static void bubble_sort(int[] array) {
        // i == 위 그림의 Round와 같음
        for (int i = 1; i < array.length; i++) {

            boolean isSwapped = false;

            // Round별 비교횟수는 Round가 증가함에 따라 횟수가 줄어듬
            for (int j = 0; j < array.length - i; j++) {

                // 현재원소가 다음원소보다 크다면 두개의 값을 변경한다.
                if (array[j] > array[j + 1]) {
                    swap(array, j);
                    isSwapped = true;
                }
            }

            // 비교과정에서 한번도 교환이 이루어지지않았다면 반복문 종료
            if (!isSwapped) {
                break;
            }

        }
    }

    private static void swap(int[] array, int index) {
        int temp = array[index];
        array[index] = array[index + 1];
        array[index + 1] = temp;
    }
}

 

+ Recent posts