반응형

조슈아 블로크의 'Effective Java' 책을 읽고 제멋대로 정리한 내용입니다. :)

 

1. Comparable이란?

 

1.1. 정의

 객체를 비교할 수 있도록 만드는 인터페이스이며, compareTo 메서드를 재정의하여 비교의 기준을 제공한다.

public int compareTo(T o);

1.2. 객체를 비교한다?

 int 타입의 1과 2를 비교한다면 단순히 관계 연산자를 통해 비교하면 된다. 하지만 아래와 같은 Student 객체를 비교할 어떻게 해야할까?

 여러 값을 가진 Student 객체를 비교하기 위한 기준이 없다. number를 기준으로 할지, name을 기준으로 할지에 대한 명확한 기준 말이다. Comparable의 compareTo 메서드는 이 기준을 정의할 수 있도록, 즉, 비교의 기준을 제공할 수 있도록 하며, Arrays.sort 메서드 호출 시 이 기준을 참조하여 정렬하게 된다.

 

public class Student implements Comparable<Student>{

    private int number;
    private String name;

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

    @Override
    public int compareTo(Student o) {
        if(this.number > o.number){ // number에 대해 오름차순
            return 1;
        }

        else if(this.number == o.number){
            return 0;
        }
        else{
            return -1;
        }
    }

    @Override
    public String toString() {
        return "Student{" +
                "number=" + number +
                ", name='" + name + '\'' +
                '}';
    }
}

 

1.3. 반환 값 1, 0, -1?

 compareTo 메서드를 보면 1, 0, -1을 반환하고 있다. Arrays.sort 메서드 호출 시 내부적으로 클래스에 정의한 compareTo 메서드를 호출하는데, 이때 응답 값이 양수이면 두 객체의 위치를 바꾸고, 음수나 0이면 그대로 유지한다.

 결국 this.number 와 o.number 사이의 부등호가 > 인지, < 인지에 따라 반환 값이 달라지므로 정렬 방식이 달라지게 되는데, 정렬방식이 어떻게 달라지는지 헷갈릴 경우 다음과 같이 생각하면 편하다.

 

 예를들어 10, 21, 3의 number를 가진 Student가 있고, 이를 sort 할 경우 compareTo 메서드의 this.number는 첫 인스턴스의 number인 10일 것이고, o.number는 그 다음 인스턴스의 number인 21 일 것이다.

 그 후 정의한 compareTo 메서드를 실행시킨다고 가정하면 10 < 21 이므로 -1이 리턴되고, 순서는 그대로 유지된다. [10, 21] 순서로 정렬되며 이는 오름차순 정렬이 된다. (실제로 이렇게 동작하는 것은 아니다)

Student student1 = new Student(10, "홍길동");
Student student2 = new Student(21, "심심이");
Student student3 = new Student(3, "심심이");

Student[] arr = new Student[]{student1, student2, student3};

Arrays.sort(arr);

for(Student student : arr){
    System.out.println(student);
}

 

실행 결과

 


1.4. 뺀 값을 반환하면 안돼?

 현재 조건문에 따라 1, 0, -1을 반환하는 대신 두 변수의 차를 반환하는 것이 더 깔끔해보인다. this.number가 더 크면 양수를 반환할테고, 동일하면 0을, o.number가 더 크면 음수를 반환할 것이다. 실제 정렬에는 이 두 수의 '차이'를 이용하는 게 아닌 양수, 0, 음수인지를 이용하기 때문에 큰 문제가 없어보인다. 실제로 아래와 같이 this.number - o.number로 수정해도 그 결과는 동일하다.

 

@Override
public int compareTo(Student o) {
    return this.number - o.number; // number에 대해 오름차순
}

 

실행 결과

 

 하지만 overflow 가 발생하게 될 경우 문제가 된다. 만약 this.number가 int의 최대값인 2,147,483,647 이고 o.number가 -1일 경우 연산 결과는 양수(2,147,483,648)가 아닌 음수(- 2,147,483,648) 가 된다. 최댓값을 넘어서 overflow가 발생한 것이다. 속도도 월등히 빠르지도 않기에 이 방법은 권장하지 않고 있다.

 


2. Comparable을 구현할지 고려하자.

 본론으로 와서 Comparable의 메서드인 compareTo는 단순 동치성 비교 뿐 아니라 순서까지 비교할 수 있고, 제네릭한 성질을 갖는다. Comparable을 구현한 객체들의 배열은 앞서 언급했던 Arrays.sort() 메서드를 통해 쉽게 정렬할 수 있다.

 이런 강력한 정렬 기능을 제공하는데 필요한 건 단 하나, Comparable의 compareTo 메서드를 구현하는 것 뿐이다. 때문에 자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입이 Comparable을 구현하기도 했다.

 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 Comparable 인터페이스를 구현하는 것이 바람직하다.

 


3. 관계 연산자보다 compare를 사용하자.

 앞선 예제에서도 두 값을 비교할 때 >, == 와 같은 관계 연산자를 사용했다. 하지만 자바 7부터 박싱된 기본 타입 클래스(ex. Integer)들에 새로 추가된 compare 메서드 사용을 권장하고 있다. 내부적으로 삼항 연산자를 통해 비교 후 -1, 0, 1 값을 리턴한다.

@Override
public int compareTo(Student o) {
    return Integer.compare(number, o.number);
}

 

Integer.compare

 

 문자열을 비교할때도 마찬가지이다. Java에서 제공하는 String.CASE_INSENSITIVE_ORDER.compare 메서드를 사용하면 대소문자 구분 없이 문자열을 비교할 수 있다. 만약 number가 아닌 name에 대한 오름차순 정렬을 해야한다면 아래와 같이 코드를 수정하면 된다.

 

@Override
public int compareTo(Student o) {
    return String.CASE_INSENSITIVE_ORDER.compare(this.name, o.name);
}

 


4. 정리

 순서를 고려해야하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교하는 기능을 제공하는 컬렉션과 어우러지도록 해야한다.

 compareTo 메서드에서 필드 값을 비교할 때 <와 > 연산자를 쓰는 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드를 사용하자.

반응형
반응형

조슈아 블로크의 'Effective Java' 책을 읽고 제멋대로 정리한 내용입니다. :)

 

1. 개요

 clone 메서드의 개념과 사용법, 주의사항에 대해 알아보자.


2. clone 메서드가 뭔가요?

 

2.1. 정의

 clone 메서드는 인스턴스를 복제하는 메서드이다. 이 메서드를 사용하기 위해서는 반드시 Cloneable 인터페이스를 implements 해야한다. 이를 어길 경우 clone 메서드 호출 시 CloneNotSupportedException이 발생한다.

public class Human implements Cloneable{
	...
}

2.2. 구현할게 없는 Cloneable 인터페이스

구현할 메서드가 없는 Cloneable

 clone 메서드를 사용하기 위해 Cloneable 인터페이스를 implements 한 후 구현 메서드를 작성하기 위해  Cloneable 인터페이스를 들어가봤더니 구현할 메서드가 없다. clone 이라는 메서드조차 없다. Clonable은 JVM에게 '이 클래스는 복제 가능한 클래스다.' 라는 것을 알려주는 구분자 역할을 할 뿐이기 때문이다.

 


2.3. 그럼 clone 메서드는 어디에?

 실제 복제 기능을 수행하는 clone 메서드는 Object 클래스 존재한다. Object 클래스에 있는 clone 메서드의 사용법을 알아보자.

 


2.4. clone 메서드 사용법

 Object의 clone() 메서드는 접근 제어자가 protected 이기 때문에 자식클래스 또는 같은 패키지 내에서만 호출이 가능하다. Object의 패키지 경로는 java.lang.Object이니 결국 자식 클래스에서만 호출이 가능하다. 즉, 외부에서는 호출이 불가능한 것이다.

외부에서 접근이 불가능한 clone 메서드

 

 이에따라 클래스 내부에서 super.clone() 메서드를 호출하도록 clone 메서드를 재정의 해야한다.

public class Human implements Cloneable{

    private String name;

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

    public Human(String name){
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

 

실제로 테스트를 하면 똑같은 필드를 가진 인스턴스가 생성됨을 알 수 있다.

public static void main(String[] args) throws CloneNotSupportedException {
    Human human = new Human("테스터");
    Human cloneHuman = (Human) human.clone();
    System.out.println(human.getName());
    System.out.println(cloneHuman.getName());
}

실행 결과

 


3. 주의사항

3.1. CloneNotSupportedException 예외 전환하기

 CloneNotSupportedException 예외가 발생하는 경우는 Cloneable 인터페이스를 implements 하지 않은 상태에서 clone 메서드를 호출할때 뿐이다.

 사실 개발자 입장에서 clone 메서드를 사용한다면 Cloneable 인터페이스를 implements 해야한다는 사실을 알고있기에 CloneNotSupportedException 예외 상황은 발생 불가능한 예외라고 할 수 있다.

 그런데 이 예외는 Checked Exception이며 예외처리를 필수로 해야한다. 발생 불가능한 예외에 대해 예외처리를 하는거 자체가 웃긴 상황이다.

 예외 처리를 강제하지 않도록 예상치 못한 상황이 발생했음을 나타내는 에러인 AssertionError를 사용하여 예외를 변환해주자.

@Override
public Object clone()  {
    try{
        return super.clone();
    }catch (CloneNotSupportedException e){
        throw new AssertionError();
    }
}

 


3.2. 형변환은 clone 메서드 내에서

 현재 clone 메서드에서는 Object 타입으로 복제 후 클라이언트에서 이를 형변환하고 있다. 형변환 작업은 clone 메서드 내에서 처리한 후 해당 타입에 맞게 반환해주자.

@Override
public Human clone()  {
    try{
        return (Human) super.clone();
    }catch (CloneNotSupportedException e){
        throw new AssertionError();
    }
}

...

//Main.class
public static void main(String[] args){
    Human human = new Human("테스터");
    Human cloneHuman = human.clone();
}

 


3.3. 가변 객체를 참조하는 클래스에 대해서는 clone을 사용하면 안된다.

 아래 Stack 클래스의 경우 elements라는 가변객체를 참조하고 있다. Stack에 대해 clone을 하게 된다면, 복제된 인스턴스 내의 elements는 기존 elements와 동일한 주소를 갖는 elements를 참조할 것이다. 둘 중 하나의 인스턴스를 수정하면 다른 하나도 수정되어 프로그램이 이상하게 동작할 수 있다.

public class Stack implements Cloneable{
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack(){
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e){
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop(){
        if(size == 0){
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }

    private void ensureCapacity(){
        if(elements.length == size){
            elements = Arrays.copyOf(elements, 2* size + 1);
        }
    }

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

    }
}

 

 테스트를 통해 알아보면, 다음과 같이 1과 2를 push한 stack 인스턴스를 생성한 후 cloneStack 인스턴스로 복제한다. 그리고 cloneStack 인스턴스에 대해 3을 push 한다면 elements 값에 3이라는 값이 추가될것이고, stack과 cloneStack 모두 동일한 elements를 갖고 있으므로 두 인스턴스 모두에게 영향을 끼치게 된다. 엎친데 덥친격으로 stack의 경우 elements의 실제 사이즈는 3이나, size 변수의 값은 2 가 된다. 버그를 유발할 가능성이 매우 크다.

class Main{
    public static void main(String[] args) {
        Stack stack = new Stack();
        stack.push(1);
        stack.push(2);

        Stack cloneStack = stack.clone();
        cloneStack.push(3);

    }
}

 

cloneStatck
stack

 

 clone 메서드는 본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야한다. Stack의 clone 메서드를 제대로 동작하기 위해서는 elements 배열도 clone을 통해 복제해야 한다.

 참고로 배열의 clone은 원본 배열과 똑같은 타입의 배열을 반환하기 때문에 형변환이 필요 없다. 이러한 이유로 배열 복제 시 clone 메서드를 사용을 권장하고 있다.

@Override
protected Stack clone() {
    try{
        Stack stack = (Stack)super.clone();
        stack.elements = elements.clone();
        return stack;
    }catch (CloneNotSupportedException e){
        throw new AssertionError();
    }
}

3.4. 주의사항이 많은 clone

 clone을 올바르게 사용하기 위해 Clonable 인터페이스를 구현, clone 메서드 재정의, 예외전환, 형변환, 가변객체는 따로 clone 하는 등의 많은 주의사항을 지켜야 한다. 그런데 객체를 복사하는 방법이 clone 메서드를 사용하는 방법만 있는 건 아니다. 앞서 언급한 주의사항을 고려하지 않아도 되는 복사 생성자와 복사 팩터리라는 더 좋은 방식이 있다.

 


4. 복사 생성자와 복사 팩터리

 복사 생성자는 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자이고, 복사 팩터리는 복사 생성자를 통해 인스턴스를 제공하는 정적 팩터리 메서드이다.

 이 방법을 사용하면 Clonable 인터페이스를 구현하지 않아도 되고, clone 메서드를 재정의하지 않아도 되고,  불필요한 예외처리를 할 필요가 없고, 형변환 할 필요도 없다. 배열 타입의 가변객체의 경우 똑같이 clone을 사용하곤 있지만, 앞서 언급한 여러 방면을 비교해봤을 때 clone보다 복사 생성자와 팩터리 메서드를 사용하는 방식이 이점이 많다.

public Stack(Stack stack){
    this.elements = stack.elements.clone();
    this.size = stack.size;
}

// 복사 팩터리 메서드
public static Stack newInstance(Stack stack){
    return new Stack(stack);
}

 


5. 정리

 복제 기능은 생성자와 팩터리를 이용하는게 최고다. 단, 배열 복제 시에는 clone 사용을 권장한다.

반응형
반응형

조슈아 블로크의 'Effective Java' 책을 읽고 제멋대로 정리한 내용입니다. :)

 

1. 개요

 toString 메서드는 오브젝트 내 멤버필드를 출력하는 용도로 사용될 것이라 예상하지만, 실제로 '클래스이름@16진수 해시코드'를 반환한다. 언뜻보면 불필요해보이는 이 메서드는 무엇을 위해 사용되는 걸까?

 


2. toString의 규약

간결하면서 사람이 읽기 쉬운 형태의 유익한 정보를 반환해야 한다.

 

 toString 메서드의 목적은 위에 기재된 toString의 규약과 같이 유익한 정보를 제공하기 위함이다. 그런데 최상위 클래스인 Object의 toString 메서드를 호출하면 '클래스이름@16진수 해시코드' 를 반환한다. 이는 간결하다 할 수 있지만 유익한 정보는 아니다. 결국 규약을 지키려면 Object에 정의된 toString 을 재정의하여 유익한 정보를 반환하도록 해야한다. 실제로 toString의 다른 규약에서도 재정의를 강조하고 있다.

 

모든 하위 클래스에서 이 메서드를 재정의해야 한다.

 

3. toString의 목적

 규약을 통해 필자가 이해한 toString 메서드의 목적은 사람이 읽을 수 있는 정보를 간결하게 제공하는 것이다. 그리고 이를 위해서는 재정의라는 작업이 반드시 필요하다.


4. 자동 호출되는 toString

 클래스를 println, printf 등의 메서드로 출력할 경우 자동으로 해당 클래스의 toString 메서드가 호출된다. 결국 잘 정의된 toString을 통해 개발자에게 정보를 제공하게 된다면, 디버깅이나 로깅 시 유용하게 활용될 수 있는 것이다.

 

class Human{
    private final String name;
    private final int age;

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

    @Override
    public String toString() {
        return "Human{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}


...

public static void main(String[] args) {
	Human human = new Human("테스터",19);
    System.out.println(human); // 자동으로 toString 메서드가 호출됨.
}

자동 호출된 toString()

 

 


5. 잘 정의된 toString 

 

5.1. 객체가 가진 주요 정보를 모두 반환하는게 좋다.

 주요 정보를 모두 반환해야 하는게 좋다. 만약 일부 정보만 담겨 있다면, 특정 상황에서의 혼란을 야기할 수 있다. 아래는 Human 클래스의 멤버필드 중 일부만 반환하도록 toString()을 재정의하였다.

 (※ 테스트 시 비교에 사용될 equals와 hashCode 메서드는 올바르게 재정의하였다.)

 

public class Human {

    private final String name;
    private final int age;
    private final long height;
    private final long weight;

    public Human(String name, int age, long height ,long weight){
        this.name = name;
        this.age = age;
        this.height = height;
        this.weight = weight;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Human human = (Human) o;
        return age == human.age && height == human.height && weight == human.weight && Objects.equals(name, human.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, height, weight);
    }


    // 일부 정보만 반환하는 toString
    @Override
    public String toString() {
        return "Human{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

 

 

 assertj를 통해 두 Human 클래스를 weight만 다르게 하여 생성 후 isEquals를 통해 비교해보자.

@Test
public void test(){

    Human human1 = new Human("홍길동", 34, 203, 110);
    Human human2 = new Human("홍길동", 34, 203, 80);

    assertThat(human1).isEqualTo(human2);
}

weight 값이 다르기 때문에 테스트가 실패하는 건 당연하다. 하지만 테스트에 대한 실패 메시지를 보면 "값은 똑같은데 왜 실패하지?" 라는 의문이 들 것이다. 이는 단순히 객체의 일부 정보만 반환해서 발생했다.

 

 

 

테스트 결과

 


5.2. 너무 많은 정보를 갖는 객체는 요약 정보를 반환하라.

 객체의 정보가 너무 많다면 요약 정보를 반환하도록 해야 한다. 아래와 같이 거주자 리스트를 담고있는 Town 클래스에 대해 모든 정보를 출력하도록 toString을 재정의한다면 불필요하게 너무 많은 정보를 반환할 수도 있다. 

 

public class Town {
    private String name;
    private List<Human> residentList;

    private Town(String name, List<Human> residentList){
        this.name = name;
        this.residentList = residentList;
    }
    public static Town of(String name, List<Human> residentList){
        return new Town(name, residentList);
    }

    @Override
    public String toString() {
        return "Town{" +
                "name='" + name + '\'' +
                ", residentList=" + residentList +
                '}';
    }
}

 

 이때는 아래와 같이 요약정보를 반환할 수 있도록 재정의하자.

@Override
    public String toString() {
        return "Town{" +
                "name='" + name + '\'' +
                ", residentList Count =" + residentList.size() +
                '}';
    }
}

출력 결과

 


5.3. 반환 값의 포맷을 지정하지 말자

 전화번호나 행렬 같은 값 클래스의 경우 반환 값이 데이터의 성질에 따라 포맷팅 될 수 있다. 예를들어 전화번호의 경우 아래의 포맷을 가질 수 있다.

@Override
public String toString(){
    return String.format("%03d-%03d-%04d",
            areaCode, prefix, lineNum);
}

 이렇게 포맷이 정의되어 있으면 포맷 형식에 맞게 문서 형식을 만들고 재가공하는 2차 작업이 있을 수 있다. 이때 포맷이 바뀌게 된다면 이러한 작업에 영향을 미치게 된다. 반대로 2차적인 작업에 영향을 미치게 하지 않기 위해 데이터가 포맷에 의존하도록 한정될 수도 있다.

 만약 작업이 있다면 toString을 통해 포맷된 데이터를 가져오거나, toString을 통해 원본 데이터를 가져오고 이를 포맷팅하지는 메서드를 따로 만들지 말고, toString이 반환할 값을 얻어올 수 있는 API를 따로 제공하는게 바람직하다.

 


5.4. IDE에서 제공하는 기능을 활용하자.

 객체의 모든 정보를 반환해주는 toString 메서드는 여러 IDE에서 기본으로 제공한다. 객체의 정보를 알려주지 않는 Object의 toString 메서드를 사용하지 말고, IDE에서 제공하는 toString을 사용하는 것도 좋은 방법이다.

 


6. 정리

 모든 클래스에서 toString을 재정의하자. 단, 상위 클래스에서 이미 알맞게 재정의한 경우는 예외이다. toString은 객체에 관한 명확하고 유용한 정보를 읽기 좋은 형태로 반환해야 하며, 이러한 toString은 디버깅와 로깅에 유용하게 사용될 수 있다.

 

반응형
반응형

1. 개요

아키텍처는 여러 가지 방식으로 정의되고 이해될 수 있는 용어다. 가장 단순한 정의는 아래와 같다.

 아키텍처란 어떤 경계 안에 있는 내부 구성요소들이 어떤 책임을 갖고 있고, 어떤 방식으로 서로 관계를 맺고 동작하는지를 규정하는 것이다.

 

아키텍처는 관계와 동작의 규정이므로 많고 다양하다. 그 중 웹 개발에 많이 사용하는 계층형 아키텍처와 오브젝트 중심 아키텍처에 대해 알아보았다.

 


2. 계층형 아키텍처

 책임과 성격이 다른 것을 그룹으로 만들어 분리해두는 것을 아키텍처 차원에서는 계층형 아키텍처라고 한다. 보통 웹 기반의 계층은 데이터 액세스 계층, 서비스 계층, 프레젠테이션 계층, 이 세 개의 계층을 갖는다고 해서 3계층 애플리케이션, 3-tire 애플리케이션 이라고도 한다.

 

 

1) 데이터 액세스 계층(DAO)

 데이터 액세스 계층은 DB, ERP, 레거시 시스템 등에 접근하는 역할을 주로 처리하는 계층이다.

 

2) 서비스 계층

  핵심 비지니스 로직을 처리하는 계층이다. 이 계층의 클래스는 POJO로 작성되기에 객체지향적인 설계 기법이 적용된 코드를 통해 쉽게 테스트하고 유연하게 확장할 수 있다.

 

3) 프레젠테이션 계층

 가장 복잡한 계층이며, 매우 다양한 기술과 프레임워크의 조합을 가질 수 있다. 엔터프라이즈 애플리케이션에서는 HTTP 프로토콜을 사용하는 서블릿이 바탕이 된다.

 


3. 계층형 아키텍처 설계 원칙

 

가장 중요한 설계 원칙은 응집도가 높으면서 다른 계층과는 낮은 결합도를 유지하는 것이다. 각 계층은 자신의 계층의 책임에만 충실해야 한다. 예를들어 아래의 코드는 다른 계층과의 결합도를 갖는 코드이다.

 

public ResultSet findUserByName(String name) throws SQLException;

 

 서비스 계층이 DAO를 호출할 때 ResultSet를 처리해야 한다면, 이는 서비스 계층이 JDBC라는 특정 데이터 액세스 계층 기술에 종속되는 것이다.

 예외도 마찬가지이다. SQLException은 Checked Exception이다. 서비스 계층에서는 이를 무시할 수 없기에 예외 상황을 분석하기 처리하는 코드를 만들어야 한다. 이 코드는 다음과 같이 수정돼야 한다.

 

public List<User> findUserByName(String name) throws DataAccessException;

 User는 사용자 정보를 담는 오프젝트이므로 특정 계층에 종속되지 않는다. 결과는 이렇게 계층에 종속되지 않는 오브젝트로 반환해야 한다.

예외 또한 DataAccessException과 같은 런타임 예외로 만들어야 한다. 이를 통해 서비스 계층에서는 이에 대한 예외처리를 하지 않아도 된다.

 

 이 원칙에 위배되는 흔한 실수 중 하나가 프레젠테이션 계층의 오브젝트를 서비스 계층으로 전달하는 것이다. HttpServeltRequest나 HttpSession과 같은 타입을 서비스 계층 인터페이스 메서드의 파라미터 타입으로 사용하면 안된다.

 웹 방식이 아닌 클라이언트가 이 비지니스 로직을 사용해야 할 경우 재사용이 불가능하며, 단위테스트가 복잡해지는 단점이 있다.

 


4. DB/SQL 중심의 로직 구현 방식

 쉽게 말해서 비지니스 로직을 SQL 을 통해 처리하는 방식이다. 로직이 복잡해지면 SQL이 복잡해지고, 하나의 트랜잭션 단위마다 하나 이상의 SQL이 생성되어야 한다. 이러한 방식은 자바를 DB와 연결해주는 단순한 인터페이스 도구로 전락시키는 것이다.

 


5. 거대한 서비스 계층 방식

 복잡한 SQL을 피하면서, 핵심 비지니스 로직은 서비스 계층에서 처리하는 방식이다. DAO가 응답한 정보를 분석, 가공하는 것이 서비스 계층의 핵심 코드가 된다. 이처럼 어플리케이션 코드에 비지니스 로직이 담겨있기 때문에 자바 언어의 장점을 잘 활용하는 것이고, 테스트하기 용이하다.

 하지만 DAO 계층의 SQL은 서비스 계층의 비지니스 로직의 필요에 따라 만들어지기 쉽기 때문에 강한 결합을 여전히 갖고 있다. 서비스 계층의 코드도 업무 단위로 만들어지므로 DAO를 공유할 수 있는 것을 제외하면 코드의 중복이 발생할 수 있다.

 


6. 오브젝트 중심 아키텍처

 오브젝트를 만들고 오브젝트 구조 안에 정보를 담아서 각 계층 사이에 전달하게 하는 아키텍처 방식이다.

 재사용하기 쉽고, 전 계층에서 일관된 구조를 유지한채 사용할 수 있다.

 

 하지만 최적화된 SQL을 사용하지 못하고, 멤버필드가 많아지면 사용하지 않는 필드가 증가하므로 데이터 중심 아키텍처보다 성능이 떨어질 수 있다. 이러한 문제는 지연 로딩(Lazy Loading)을 통해 어느정도 해결할 수 있으며, 실제로 JPA, 하이버네이트와 같은 ORM 기술에서 지연 로딩을 많이 사용한다.

 

 오브젝트에는 단순히 정보 뿐 아니라 기능도 함께 담고있어야 한다. 그래야 서비스 계층의 비지니스 로직에서 재사용할 수 있기 때문이다.

반응형
반응형

1. 생성보다는 재사용을 고려하자.

 똑같은 기능의 객체를 매번 사용하기보다는 객체 하나를 재사용하는 편이 낫다. 실제로 Boolean.valueOf() 는 호출할 때마다 객체를 생성하지 않고 내부에 캐싱된 객체를 사용한다.

Boolean.valueOf("true");

 

 우리는 문자열을 초기화할때 매번 new 생성자를 통해 생성하지 않는다. 하나의 String 인스턴스를 사용한다. 이 경우 JVM의 String pool이라는 문자열 풀에서 캐싱하게 되고, 재사용하게 된다.

String a = "abc";
String b = "abc";

System.out.println(a == b); // String Pool에서 조회 : true

String c = new String("abc");
String d = new String("abc");

System.out.println(c == d); // Heap 메모리에 새로 생성된 객체를 조회 : false

 

 


 

2. 성능을 향상시키는 재사용

 재사용을 통해 성능이 향상되는 케이스를 알아보자.


2.1. String.matcher()

static boolean isRomanNumber(String s){
    return s.matches("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}

 들어온 문자열이 로마 숫자인지를 체크하는 메서드이다. 보면 String.matches() 메서드가 호출되는데 내부에서 입력받은 문자열를 통해 Pattern 인스턴스를 생성한다. Pattern은 인스턴스 생성 비용이 높다. 즉, 이 메서드를 호출할 때마다 비용이 비싼 인스턴스를 생성 후 한번만 사용하고 버리는 것이다.

String.matches()
Pattern.compile()

 

 이를 개선하기 위해 Pattern 인스턴스를 캐싱해두고 재사용하는 코드로 변경하였다. 책에서는 실제 속도를 비교해보니 개선 전에 비해 6.5 배의 성능 향상을 가져왔다고 한다.

private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");

static boolean isRomanNumber(String s){
    return ROMAN.matcher(s).matches();
}

 

 


2.2. 오토박싱 거둬내기

 오토박싱은 프리미티브 타입과 레퍼런스 타입을 섞어 쓸때 상호 변환해주는 기술이다. 코드에서 Long 타입의 sum과 long 타입의 i를 연산하는 과정에서 오토박싱이 발생하여 연산 결과를 Long 인스턴스로 생성하게 된다. Integer.MAX_VALUE가 '2의 31승 -1' 이므로, 이 만큼의 Long 인스턴스가 새로 생성되는 것이다. 실행 시간은 6초가 걸렸다.

Long sum = 0L;

long start = System.currentTimeMillis();
for(long i = 0; i <= Integer.MAX_VALUE; i++){
    sum += i;
}

System.out.println(sum);
System.out.println(System.currentTimeMillis() - start);

 

 불필요한 Long 인스턴스의 생성을 막으려면 오토박싱을 거둬내면 되며, sum의 타입을 Long에서 long으로 바꿔주면 된다. 수정 후 실행 시간 0.6초가 걸렸다. 오토박싱이 적용된 코드보다 약 10배 빨라졌다.

public static void main(String[] args) {
    long sum = 0L; // 수정

    long start = System.currentTimeMillis();
    for(long i = 0; i <= Integer.MAX_VALUE; i++){
        sum += i;
    }

    System.out.println(sum);
    System.out.println(System.currentTimeMillis() - start);
}

 


3. 혼란을 야기할 수 있는 재사용

 객체가 불변이라면 재사용해도 안전하다. 이러한 특성을 살려 자바의 기본 라이브러리에서 많이 활용하고 있는데 만약 개발자가 이런 활용 사실을 인지하지 못할 경우 오히려 혼란을 야기할 수 있다. 

 

3.1. 어댑터

 어댑터는 실제 작업은 뒷단 객체에 위임하고, 자신은 제2의 인터페이스 역할을 해주는 객체이다. 예를들어 Map 인터페이스의 keySet 메서드는 Map 안의 키를 전부 담은 어댑터를 반환한다. 다시말하면 키를 가진 객체를 새로 생성하는게 아닌 내부 객체를 재사용하여 키 정보를 반환하는 것이다.

 

 앞서 keySet 메서드는 키 정보를 내부에서 가져온다고 했지만, 필자의 경우 키를 담은 객체를 생성하여 반환하는 줄 알았다. 만약 필자와 같이 keySet의 어댑터 특성을 몰랐다면 keySet() 메서드를 활용해 두개의 키 셋을 구하고 이를 각각 활용해야 하는 상황에서 아래와 같은 결과에 대해 혼란을 느낄 수 있을 것이다.

 

Map<String, Integer> map = new HashMap<>();
map.put("도끼", 3);
map.put("활",1);

Set<String> set1 = map.keySet();
Set<String> set2 = map.keySet();

set1.remove("도끼");

System.out.println(set1.size()); // 1
System.out.println(set2.size()); // 1 (필자는 2를 예상했다 !)

 

 첨언으로 이러한 상황에서는 keySet 메서드처럼 객체의 주소를 복사하여 사용하는 방식이 아닌, 객체의 내부 값을 참조하여 복사하는 '방어적 복사' 방식을 사용해야 한다.


4. 정리

 이번 아이템은 "객체 생성은 비싸니 피해야한다"가 아니다. 요즘 JVM에서는 작은 객체를 생성하고 회수하는 일이 크게 부담되지 않기 때문이다.

 중요한 건 방어적 복사가 필요한 상황에서 객체를 재사용했을 때의 피해가 필요 없는 객체를 반복 생성했을 때의 피해보다 훨씬 크다는 사실을 인지하고 재사용을 고려해야 하는 것이다.

반응형
반응형

조슈아 블로크의 'Effective Java' 책을 읽고 제멋대로 정리한 내용입니다. :)

 

1. 개요

 많은 클래스가 하나 이상의 자원에 의존한다. 가령 맞춤법 검사기는 사전에 의존하는데, 이런 클래스를 정적 유틸리티 클래스나 싱글톤 클래스 구현한 모습을 드물지 않게 볼 수 있다.

 

1) 정적 유틸리티 클래스로 구현한 예

public class SpellChecker {
    
    // 의존 객체 생성
    private static final KoreaDictionary dictionary = new KoreaDictionary();
    
    private SpellChecker(){} // 외부 객체 생성 방지
    
    public static boolean isValid(String word){
        ...
    }
    
    public static List<String> suggestions(String typo){
        ...
    }
}

 

2) 싱글톤 클래스로 구현한 예

public class SpellChecker {

	// 의존 객체 생성
    private final KoreaDictionary dictionary = new KoreaDictionary();
    
    // 싱글턴 패턴을 사용해 생성한 인스턴스
    public static SpellChecker INSTANCE = new SpellChecker(); 
    
    private SpellChecker(){
    }

    public static boolean isValid(String word){
        ...
    }

    public static List<String> suggestions(String typo){
       ...
    }

}

 


2. 위 방식의 문제

 두 방식 모두 한국어 사전만 사용한다면 큰 문제가 되지 않지만 다른 종류의 사전을 사용해야한다면 변경이 필요할 때마다 의존 객체 생성 부분 코드를 수정해야한다.

 


3. 수정자 메서드를 통한 문제 해결?

 수정자 메서드를 추가하여 의존객체를 변경하는 방법이 있다. 이를 사용하면 사전의 종류가 바뀌는 건 맞지만, 멀티쓰레드 환경에서는 변경되는 상태 값을 공유하면 안되므로 사용해선 안된다.

 결국 멀티쓰레드 환경에서는 의존객체가 변경되는 클래스를 싱글톤이나 정적 유틸리티 클래스로 구현하면 안된다.

그렇다면 이러한 클래스는 어떻게 구현해야할까? 바로 생성자를 통해 의존 객체를 주입해야한다.

 


4. 생성자를 통한 의존 객체 주입

public class SpellChecker {
    
    private final Dictionary dictionary;
    
    // 생성자를 통한 의존 객체 주입
    public SpellChecker(Dictionary dictionary){
        this.dictionary = dictionary;
    }

    public static boolean isValid(String word){
        ...
    }

    public static List<String> suggestions(String typo){
        ...
    }
}

 생성자를 통해 객체를 생성할 때만 의존 객체를 주입하고 있다. 의존 객체는 불변성을 갖게 되어 멀티 쓰레드 환경에서도 안심하고 사용할 수 있다.

 KoreaDictionary 클래스 대신 Dictionary 인터페이스를 사용하고, KoreaDictionary와 같은 여러 사전 클래스들이 Dictionary 인터페이스를 구현하도록 했다. 만약 영어사전에 대한 맞춤법 검사 기능을 제공해야한다면 Dictionary 인터페이스를 구현한 EnglishDictionary 클래스를 만들고, 이를 외부에서 생성자를 통해 주입하면 된다. 이로써 의존 객체가 바뀌더라도 SpellChecker의 코드는 변경할 필요가 없게 되었다.

 

 이렇게 의존성을 외부로부터 주입받는 패턴을 의존 객체 주입 패턴이라고 하며 이 패턴의 변형으로 자바 8에서 등장한 Supplier<T> 인터페이스가 있다. 한정적 와일드 카드 타입을 사용해 팩터리의 타입 매개변수를 제한하여 사용할 수 있으며, 클라이언트에서는 해당 타입의 하위 타입을 포함하여 무엇이든 생성할 수 있는 팩터리를 넘길 수 있다.

 

public class SpellChecker {

	...
    public SpellChecker(Supplier <? extends Dictionary> factory){
        this.dictionary = factory.get();
    }
    
    ...
}

 

SpellChecker spellChecker = new SpellChecker(() -> new KoreaDictionary());

 

※ 개방 폐쇄 원칙

 SpellChecker의 맞춤법 검사 기능이 영어까지 '확장' 되었지만, SpellChecker의 코드 '변경'은 일어나지 않는다. 이처럼 '확장'에는 열려있고, '변경'에는 닫혀있는 것을 '개방 폐쇄 원칙' 이라고 한다.


 

5. 정리

 클래스가 하나 이상의 객체에 의존한다면 확장성을 고려하여 싱글턴과 정적 유틸리티 클래스 형태로 사용하지 않는 것이 좋다. 의존 객체는 내부에서 직접 생성하지 말고 생성자 혹은 정적 팩터리 메서드를 통해 넘겨주는 것이 좋다. 의존 객체 주입은 변경에 대한 유연성, 재사용성, 테스트 용이성을 제공한다.

 

 

반응형
반응형

조슈아 블로크의 'Effective Java' 책을 읽고 제멋대로 정리한 내용입니다. :)

 

1. 개요

 필자는 클래스를 정의할 때 매개변수가 많으면 Builder를, 매개변수가 많이 없으면 생성자 메서드를 사용한다. Setter 메서드는 존재 자체로 객체의 상태 변경 가능성을 열어두기 때문에 사용을 지양하고 있으며, 상태 변경이 필요할 경우 Setter 대신 다른 Update와 같이 다른 네이밍을 사용하여 '비지니스적으로 필요한 상태변경이 있을 수 있다' 라는 것을 암시해주었다.

 이번 아이템에서는 필자가 알고있는 내용을 다시한번 정리했고, 빌더를 사용하는 이유와, Setter보다 어떤면에서 더 효율적인지를 이해하게 되었다. 

 


2. 자바 빈즈 패턴 

 

2.1. 자바 빈즈 패턴이란?

 자바 빈즈 패턴이란 기본 생성자로 객체를 만든 후, Setter 메서드를 호출해 원하는 매개변수의 값을 결정하는 방식이다. 인스턴스를 만들기 쉽고, 사용법도 매우 간단하지만 이 방식은 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓인다는 매우 심각한 단점을 갖고 있다.

public class NutritionFact {
    private int servingSize;
    private int servings;
    private int calories;
    private int fat;
    private int sodium;
    private int carbohydrate;

    public void setServingSize(int servingSize) {this.servingSize = servingSize;}

    public void setServings(int servings) {this.servings = servings;}

    public void setCalories(int calories) {this.calories = calories;}

    public void setFat(int fat) {this.fat = fat;}

    public void setSodium(int sodium) {this.sodium = sodium;}

    public void setCarbohydrate(int carbohydrate) {this.carbohydrate = carbohydrate;}
}

 


2.2. 일관성이 뭔가요?

 일관성(Consistency)이란 객체의 상태가 프로그램 실행 도중 항상 유효한 상태를 유지해야 한다는 원칙을 말한다. 즉, 객체 내부 상태가 항상 유효한 상태로 일관되게 유지되어야 한다는 뜻이다.

 

 즉, 객체의 상태 값이 할당이 되지 않거나, 할당이 되더라도 유효한 상태가 아닌 것이다. 후자의 경우 은행 계좌 잔고를 예로 들 수 있는데, 잔고는 항상 0 이상의 값으로 일관되게 유지되어야 한다. 이를 위해 아래와 같이 유효성 검사 역할을 하는 비지니스 로직을 넣기도 한다. 이처럼 객체의 상태를 유효한 상태로 일관성 있게 유지하는 것이다.

public class BankAccount{

    private double balance;
    
    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    } 
    
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
    
    // 출금 시 balance에 대한 일관성 유지를 위해 유효성 검사하는 로직이 추가됨
    public boolean withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            return true;
        }
        return false;
    }
    
    ...
}

 


2.3. 자바 빈즈 패턴은 왜 일관성이 무너지나요?

 그럼 자바 빈즈 패턴을 사용할 경우 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태. 즉, 유효하지 않은 상태가 되는 이유는 뭘까? 필수적으로 설정되어야할 객체의 상태 값이 설정되지 않을 수 있기 때문이다. 이를 책에서는 '객체가 완전히 생성되기 전' 이라고 표현하고 있다.

 

 예를들어 객체를 생성한 후 실수로 setServings()을 호출하지 않는다면 servings를 사용하는 어떤 비지니스 로직이 있을 때 예상치 못한 버그가 발생할 수 있다. 컴파일 단계에서 에러가 나는것도 아니기에 이를 사전에 인지하지 못한다면 버그를 찾기 힘들 수 있다. 참고로 아래 예제에서는 예외 또한 발생하지 않는다. int의 경우 기본 값인 0이 설정되기 때문이다. 이 모든 건 단순 Setter 메서드 호출 누락으로 인해 발생했다.

    NutritionFact nutritionFact = new NutritionFact();
    //nutritionFact.setServings(20); 실수로 누락
    nutritionFact.setServingSize(1);
    nutritionFact.setCalories(1);
    nutritionFact.setFat(10);
    nutritionFact.setCarbohydrate(10);
    nutritionFact.setSodium(10);
    
    ...
    
    int servings = nutritionFact.getServings();
    if(servings >= 10){
        // 1회 제공량이 10개 이상일 경우에 대한 비지니스 로직
    }

 

 그렇다면 위 단점을 극복하는 방법은 뭘까? 바로 일관성을 보장하는 빌더 패턴을 사용하는 것이다.

 


3. 빌더 패턴

 빌더 패턴은 객체를 생성하는 과정을 보다 유연하게 하기 위한 디자인 패턴 중 하나로, Builder라는 내부 클래스를 통해 객체를 생성하는 방법이다.

 NutritionFacts 의 생성자 메서드를 보면 알 수 있듯이 Builder를 통해서만 객체를 생성하고 있고, Builder의 생성자 메서드를 통해 servings나 servingSize와 같은 필수 값을 받도록 설정한다. 나머지 값은 체이닝 메서드 설정하도록 하고 있다. 

 

public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder{

        // 필수 매개변수는 final
        private final int servings;

        // 선택 매개변수는 기본값으로 초기화
        private int servingSize;
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;

        public Builder(int servings){
            this.servings = servings;
        }

        public Builder servingSize(int val){
            servingSize = val;
            return this;
        }

        public Builder calories(int val){
            calories = val;
            return this;
        }

        public Builder fat(int val){
            fat = val;
            return this;
        }

        public Builder sodium(int val){
            sodium = val;
            return this;
        }

        public Builder carbohydrate(int val){
            carbohydrate = val;
            return this;
        }

        public NutritionFacts build(){
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder){
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

 

 여기서 생성된 NutritionFacts 객체는 필수 값인 servings 값이 없을 수 없다. 누락시킬 경우 컴파일 단계에서 에러가 발생하기 때문이다. 즉, Builder를 통해 생성한 인스턴스는 일관성이 보장되는 것이다.

필수 값인 servings를 설정하지 않았을때 에러 발생

 


4. 자바 빈즈 패턴 + 명시적 생성자

 그럼 자비 빈즈 패턴에서 명시적 생성자를 통해 필수 값을 설정하면 되지 않을까? 그래도 된다. 이 경우 필수 값에 대한 일관성을 보장할 수 있다.

public class NutritionFact {
    private int servingSize;
    private int servings;
    private int calories = 0;
    private int fat = 0;
    private int sodium = 0;
    private int carbohydrate = 0;

    public NutritionFact(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate){
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }
    
    ...
}

 

 그리고 사용 시 아래처럼 생성자 메서드의 파라미터의 순서에 맞게 값을 입력하기만 하면 된다. 그런데 문제가 있다. 이것만 봐서는 어떤 파라미터가 객체의 멤버필드에 매핑되는지 바로 알 수 없다. 직접 생성자 메서드를 확인해야하고, 순서를 일일이 세어야 하는 수고가 필요하다.

NutritionFact nutritionFact = new NutritionFact(10, 10,1,1,1,1);

 

 이에 반해 빌더 패턴은 체인 메서드를 통해 설정하기 때문에 어떤 멤버필드에 값을 설정하는지 바로 알 수 있어 가독성을 향상시킨다. 또한 Setter 메서드가 없으니 중간에 객체의 상태가 변경되지 않음을 보장한다. 즉, 안전한 객체가 되는것이다.

new NutritionFacts.Builder(10)
        .calories(100)
        .sodium(10)
        .carbohydrate(10)
        .servingSize(10)
        .fat(1)
        .build();

 


5. 정리

 생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다. 빌더는 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.

반응형
반응형

조슈아 블로크의 'Effective Java' 책을 읽고 제멋대로 정리한 내용입니다. :)

 

1. 개요

 인스턴스를 생성할 때 생성자를 많이 사용하는데, '정적 팩터리 메서드'를 사용하기도 한다. 책에서는 정적 팩터리 메서드 사용을 권장하고 있는데, 과연 어떤 장점을 갖고 있길래 이를 권장하는 것일까?

 


2. 정적 팩터리 메서드가 뭔가요?

자바에서의 '팩터리'는 '인스턴스를 만드는 공장' 을 의미한다. 즉, 정적 팩터리 메서드란 인스턴스를 만드는 역할을 하는 Static 메서드이다. 아래와 같이 말이다.

 

public class User {

    private String name;

    // 접근 제한자를 private 로 하여 외부 호출을 막음.
    private User(String name){
        this.name = name;
    }
    
    // 유저 인스턴스를 생성하는 정적 팩터리 메서드
    public static User from(String name){
        return new User(name);
    }
}

 

 정적 팩터리 메서드 안에서 생성자를 호출하고 있고, 생성자는 private로 하여 외부 호출을 막고있다. public 생성자를 사용했을때보다 코드가 늘어나고 복잡해진것 같은데, 과연 어떤 장점을 갖고 있길래 생성자보다 정적 팩터리 메서드 방식을 권장하는 걸까?

 


3. 장점

 

3.1. 이름을 지정함으로써 가독성을 증가시킨다.

 생성자는 이름이 없다. 굳이 따지자면 클래스 명이다. 이에 반해 정적 팩터리 메서드는 이름을 지정할수 있다. 이름과 대학 입학년도를 갖는 Student 클래스로 예를들어 설명하겠다. 멤버필드인 name은 학생 이름, admissionYear는 입학년도이다.

 

1) public 생성자 사용

public class Student {

    private String name;
    private int admissionYear;

    public Student(String name){
        LocalDate now = LocalDate.now();
        this.name = name;
        this.admissionYear = now.getYear();
    }

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

class Main{
    public static void main(String[] args) {
        Student student1 = new Student("김철수");
        Student student2 = new Student("곽영희", 2020);
    }
}

 

2) 정적 팩터리 메서드 사용

public class Student {

    private String name;
    private int admissionYear;

    private Student(String name, int admissionYear) {
        this.name = name;
        this.admissionYear = admissionYear;
    }

    public static Student createFreshman(String name) {
        LocalDate now = LocalDate.now();
        return new Student(name, now.getYear());
    }

    public static Student createOfAdmissionYear(String name, int year) {
        return new Student(name, year);
    }
}

class Main{
    public static void main(String[] args) {
        Student student1 = Student.createFreshman("김철수");
        Student student2 = Student.createOfAdmissionYear("곽영희", 2020);
    }
}

 

 먼저 생성자의 신입생과 재학생에 대한 학생 인스턴스를 생성할 때 시그니처가 다른 public 생성자를 호출하고 있다. 

 여기서 중요한 점은 메인 메서드에서 이를 사용할 때 public 생성자의 시그니처만 봐서는 어떤 특성을 갖는 인스턴스를 생성하는지 알 수 없다. 직접 생성자 코드를 봐야 알 수 있다.

 

 반면 정적 팩터리 메서드를 사용한 경우 메서드 명을 통해 생성할 인스턴스의 특성을 묘사할 수 있다. createFreshman은 올해 입학한 학생, createOfAdmissionYear는 특정 년도에 입학한 학생 인스턴스를 생성하고 있다. Student라는 생성자 명만 덩그러니 있는것보다 특성을 묘사할 수 있는 메서드 이름을 사용함으로써  가독성을 증가시킨 것이다.

 


3.2. 인스턴스 재활용을 통한 메모리 효율 증대

 인스턴스를 미리 만들어 놓거나, 새로 생성한 인스턴스를 캐싱하여 불필요한 객체 생성을 피할 수 있다. 대표적으로 Boolean.valueOf(boolean) 메서드는 미리 만들어 놓은 인스턴스를 리턴하는 방식으로 사용된다.

 만약 생성 비용이 큰 객체가 자주 생성될 경우에 이 방식을 활용한다면 객체 생성 시 사용하게 되는 힙 메모리의 사용율을 줄일 수 있어 메모리 효율적이라고 할 수 있다.

public final class Boolean{

    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);
    
    ...
    
    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }
    
}

 


3.3. 하위 타입 객체 반환을 통한 유연성 증대

 Java 8버전부터 인터페이스 내에 정적 팩터리 메서드를 구현할 수 있다. 이를 통해 인터페이스의 구현체 클래스를 메서드마다 자유롭게 지정할 수 있어 인스턴스 생성에 대한 유연성을 제공한다.

 아래는 Weapon 인터페이스에 정의한 정적 팩터리 메서드를 통해 Weapon의 구현체 클래스인 Sword, Gun에 대한 인스턴스를 생성하고 있다.

public interface Weapon {
    static Weapon createSword(){
        return new Sword();
    }
    
    static Weapon createGun(){
        return new Gun();
    }
}

...

public class Main {

    public static void main(String[] args) {
        Weapon sword = Weapon.createSword();
        Weapon gun = Weapon.createGun();
    }
}

3.4. 조건에 따른 하위 타입 객체 반환을 통한 유연성 증대

 앞에서는 구현체 클래스에 대한 인스턴스 생성을 위해 외부에서 직접 해당 메서드를 호출하고 있다. 만약 요구사항이 바뀌어 직업이 검사일때는 Sword, 스나이퍼일때는 Gun 인스턴스를 생성해야한다면 어떻게 할까? main 메서드에서 직업에 대해 분기 후 검사일때는 createSword() 메서드를, 스나이퍼일때는 createGun() 메서드를 호출할 수도 있지만, 정적 팩터리 메서드 내에서 직업을 파라미터로 받고, 내부 조건문에 따라 무기 인스턴스를 반환할 수도 있다.

public interface Weapon {
    static Weapon createFromJob(Job job){
        if(job == Job.SNIPER){
            return new Gun();
        }else{
            return new Sword();
        }
    }
}

...

public class Main {

    public static void main(String[] args) {
        Weapon sword = Weapon.createFromJob(Job.SWORDS_MAN);
        Weapon gun = Weapon.createFromJob(Job.SNIPER);
    }
}

 


3.5. 캡슐화를 통한 코드중복 및 파편화 방지

 위 예제를 보면 Weapon 인터페이스에서 무기 생성에 대한 구현부를 캡슐화시키고, 메인 메서드에서는 캡슐화된 메서드를 호출하고 있다. 그렇다면 캡슐화란 무엇이고 목적은 뭘까?

 


※ 캡슐화

캡슐화란 객체의 속성과 행위를 하나로 묶고, 실제 구현부는 외부로부터 은닉하는 것을 말한다.

 

환자가 약사에게 독감 진단서를 제출하면 약사는 정해진 재료와 제조 과정을 거쳐 약을 조제한다. 알약을 받는 환자는 여기에 사용된 재료나 제조 과정을 이해할 필요도, 알 필요도 없다. 다만 진단서를 제출할 뿐이다.

 

 자바 코드로 이해하면 약사 객체인 Chemist 클래스를 만들고 약을 제조하는 전 과정을 makePill 이라는 약을 조제하는 메서드로 묶는 것이다. 외부 클래스에서 약이 필요하다면 Chemist 클래스에서 해당 메서드를 호출하기만 하면 된다. 호출하는 클래스는 어떤 과정을 거쳐 약이 만들어지는지는 알 필요가 없게 된다. 

 이처럼 약을 조제하는데 필요한 여러 속성들과 행위를 makePill 이라는 하나의 메서드로 묶고, 이에 대한 구현부는 외부로부터 은닉하여 알 필요가 없게하는 것이 바로 캡슐화이다.

 

public class Chemist {
    public Pill makePill(String 진단서){

        // 약을 만드는 방법에 대한 로직

        return pill; // 생성된 약
    }
}

 

 만약 약사라는 클래스를 통해 해당 로직을 캡슐화를 하지 않는다면 어떤 일이 벌어질까? 약이 필요한 모든 곳에서 약을 조제하는 로직을 구현해야 하며, 아래와 같이 중복코드와 메서드 파편화가 발생하게 된다.

public void hospitalCare(){

    // 1. 병원 진료

    // 2. 약은 만드는 방법에 대한 로직 >> 코드중복 ! 메서드 파편화!

}

// 병원 진료 내역을 조회한 후 해당 내역에 대한 처방전을 받는 메서드
public void getHospitalCareHistory(){

    // 1. 병원 진료 내역 조회

    // 2. 약은 만드는 방법에 대한 로직 >> 코드중복 ! 메서드 파편화!

}

 

 이 상태에서 약을 만드는 방법에 대한 로직이 변경되면 어떻게될까? 모든 메서드의 로직을 전부 바꿔야한다. 이때 작은 실수가 발생한다면 심각한 버그가 발생하게 된다. 만약 캡슐화가 되어있다면? 앞서 약사 클래스의 makePill 메서드만 변경해주면 된다.

 정리하면 캡슐화는 구현부를 외부로부터 은닉함으로써 책임을 분리하고, 코드의 중복 및 파편화를 예방하고, 유지보수하기 용이한 코드로 만들어주는 유용한 프로그래밍 기법인것이다.

 


4. 정적 팩터리 메서드 명명 방식

 메서드 명은 개발자 마음이지만, 정적 팩터리 메서드 같은 특별한 메서드의 경우 권장하는 명명 방식이 있다.

 


4.1. from

 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 메서드이다.

Date d = Date.from(instant);

4.2. of

 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 메서드이다.

Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);

4.3. instance, getInstance

 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않는 메서드이다. 즉 내부에 캐싱된 인스턴스를 리턴할 수도 있다는 말이다.

StackWalker luke = StackWalker.getInstance(options);

4.4. create, newInstance

 매번 새로운 인스턴스를 반환함을 보장한다.

Object newArray = Array.newInstance(classObject, arrayLen);

 

 

반응형
반응형

1. HTTP 메시지

 HTTP 메시지는 애플리케이션 간 주고받는 데이터의 블록들로 시작줄, 헤더, 본문으로 구성된다. 이 데이터 블록 안에 어떤 데이터들이 있는지 알아보고, 요청 메시지와 응답 메시지의 데이터가 약간 다르기때문에,이 차이도 알아보도록 하자.

 

HTTP 메시지 형태 (출처 : https://developer.mozilla.org/ko/docs/Web/HTTP/Messages)

 

 


2. 시작줄

 요청 메시지의 시작줄에는 메서드, URL, HTTP 버전이, 응답 메시지의 시작줄에는 HTTP 버전, 상태 코드, 사유 구절 정보가 포함된다.


2.1. 메서드

 메서드는 서버에게 어떤 형식의 작업을 해야하는지 알려준다.

메서드 설명
GET 서버에서 데이터를 가져온다.
HEAD 서버에서 데이터에 대한 헤더만 가져온다.
POST 서버가 처리해야할 데이터를 보내거나, 새로 저장시킨다.
PUT 서버에 요청 메시지의 본문을 (덮어)저장한다.
PATCH 서버에 저장된 데이터의 일부분을 수정한다. (2010년 RFC 표준화됨)
TRACE 메시지가 프락시를 거쳐 서버에 도달하는 과정을 추적한다.
OPTIONS 어떤 메서드를 지원하는지 확인한다.
DELETE 서버에서 데이터를 제거한다.

 


2.2. POST와 PUT ??

사실 위는 책의 내용을 필자가 재해석하여 쓴것이다. 책에 기재된 POST와 PUT의 설명을 보고 바로 이해하기 어려웠기 때문인데, 책의 내용은 아래와 같다.

 

메서드 설명
POST 서버가 처리해야할 데이터를 보낸다.
PUT 서버에 요청 메시지의 본문을 저장한다.

 

 필자의 경우 POST는 데이터를 저장할때, PUT은 덮어쓸때 사용했었다. 그런데 POST 에는 없고 PUT에는 있는 '저장'이라는 단어가 잘 이해되지 않았다. HTTP 메서드에 대해 공식문서를 찾아본 결과 '멱등성'이라는 개념을 통해 이해하게 되었고, '저장'이라는 단어에 대해 편협하게 바라보고 있었다는 걸 깨닫게 되었다.

 

멱등성
 동일한 요청을 한 번 보내는 것과 여러 번 연속으로 보내는 것이 같은 효과를 지니고, 서버의 상태도 동일하게 유지될 때 해당 HTTP 메서드가 멱등성을 가졌다고 말한다.

 

https://developer.mozilla.org/ko/docs/Glossary/Idempotent 

 

멱등성 - MDN Web Docs 용어 사전: 웹 용어 정의 | MDN

동일한 요청을 한 번 보내는 것과 여러 번 연속으로 보내는 것이 같은 효과를 지니고, 서버의 상태도 동일하게 남을 때, 해당 HTTP 메서드가 멱등성을 가졌다고 말합니다. 다른 말로는, 멱등성 메

developer.mozilla.org

 모질라 개발자 페이지를 보면 HTTP 메서드 중 GET, HEAD, PUT, DELETE, OPTION, TRACE 는 멱등성 메서드, POST, PATCH는 비 멱등성 메서드라는 것을 알 수 있다.

 

 PUT 메서드는 Word나 한글파일에서의  '저장'과 같다고 생각했다. 처음 저장할때는 디스크에 새로 저장하지만, 이후부터는 계속 덮어쓴다. 동일한 내용을 한번 저장하는 것과 여러번 저장하는 것이 같은 효과를 지님과 동시에 서버의 상태도 동일하게 유지(리소스가 1개 -> 리소스가 1개)된다는 점에서 멱등성을 보장함을 알 수 있다.

 

  이에 반해 POST는 '다른이름으로 저장' 과 같다고 생각했다. 저장할때마다 무조건 디스크에 새로 저장하게 된다. 동일한 내용을 한번 '다른이름으로 저장'하는 것과 여러번 '다른이름으로 저장'하는 것이 같은 효과를 지니고 있지만, 리소스를 계속 생성하여 서버의 상태를 변경한다는 점에서 비멱등성이라는 것을 알 수 있다.

 

 이를 이해하니 PUT은 '서버에 요청 메시지의 본문을 저장한다'라는 내용도 이해할 수 있었다.

 


 

2.3. PATCH는 비멱등성?

 멱등성을 알아보던 중 한번의 물음표가 더 나왔다. PATCH는 특정 부분만 수정하니 당연히 멱등성 메서드일줄 알았으나 비멱등성 메서드라는 부분때문이었다. 결론은 PATCH는 로직에 따라 멱등성을 보장할 수 없기 때문에 비멱등성 메서드로 정의하고 있었다. 비멱등을 유발하는 케이스를 아래에 정리해보았다.

 

사용자의 나이를 한살 증가시키는 HTTP API가 있다고 가정하자. 이는 일부분을 변경하는 것이므로 PATCH 메서드를 사용할 것이며, 사용자의 나이에 1을 더하는 로직이 들어갈 것이다.

 만약 A라는 사용자의 나이가 5살이었다면, 한번 요청했을 때는 6살이, 두번 요청했을 때는 7살이 될 것이다. 이는 멱등성을 보장하지 않는다고 할 수 있다.

 반대로 만약 사용자의 이름을 변경하는 API가 있다고 가정하자. 마찬가지로 일부분을 변경하는 것이므로 PATCH 메서드를 사용할 것이며, 사용자의 이름을 요청 이름으로 변경하는 로직이 들어갈 것이다.
 만약 A라는 사용자의 이름을 B로 변경한다면, 한번 요청하던, 두번 요청하던 이름은 B가 될 것이다. 이는 멱등성을 보장한다고 할 수 있다.

 즉, 로직에 따라 멱등성을 보장할수도, 보장하지 못할수도 있으므로 비멱등성 메서드로 정의되는 것이다.

 


 

2.4. 상태코드

 상태코드는 서버에서 어떤 행위가 일어났는지에 대한 것을 코드로 표현한 것이다. 백단위마다 다른 종류로 분류된다.

전체 범위 분류
100 ~ 199 정보
200 ~ 299 성공
300 ~ 399 리다이렉션
400 ~ 499 클라이언트 에러
500 ~ 599 서버에러

 


2.5. 사유구절

 사유 구절은 상태 코드에 대한 설명을 말한다. 예를들어 200 상태 코드에 대해서는 OK이라는 사유구절이 포함된다.

 

2.6. HTTP 버전

 HTTP 애플리케이션들이 자신이 따르는 프로토콜의 버전을 상대방에게 말해주기 위한 수단으로 사용된다.

 


3. 헤더

 헤더는 HTTP의 요청과 응답 메시지에 더하는 추가 정보로, 이름/값 쌍의 목록으로 관리되며, 일반적으로 쿠키나 인증, 컨텐츠와 같은 메타 정보가 포함된다. 헤더는 목적에 따라 총 5가지로 분류된다.

 


3.1. 일반 헤더(General Header)

 클라이언트와 서버 양쪽 모두가 사용하는 헤더이다. 예를들어 Date 헤더는 서버와 클라이언트를 가리지 않고 메시지가 만들어진 일시를 지칭하기 위해 사용된다.

 


3.2. 요청 헤더(Request Header)

 요청 메시지를 위한 헤더이다. 예를 들어 "Accept : */*" 헤더는 서버에게 어떤 미디어 타입도 받을 수 있다는 것을 의미한다. 

 


3.3. 응답 헤더(Response Header)

 응답 메시지를 위한 헤더이다. 예를 들어 "Location : http://~" 헤더는 클라이언트에게 알려준 URL로 재 요청하라는 것을 의미한다.

 


3.4. 엔티티 헤더(Entity Header)

 본문에 대한 헤더를 말한다. 예를들어 Content-Type : text/html 헤더는 클라이언트에게 본문에 들어간 데이터가 HTML 문서라는 것을 의미한다.

 


3.5. 확장 헤더(Extension Header)

 개발자에 의해 커스텀되어 만들어졌지만 HTTP 명세에는 추가되지 않는 비표준 헤더이다.

 


4. 본문

 HTTP 메시지에 덱스트, 이미지, 비디오, HTML 문서 등 여러 종류의 디지털 데이터를 포함시켜 요청하기 위해 사용된다.

 


5. 출처

https://developer.mozilla.org/ko/docs/Web/HTTP/Messages - HTTP 메시지

https://developer.mozilla.org/ko/docs/Glossary/Idempotent - 멱등성

HTTP 완벽가이드 - 데이빗 고울리

 

반응형
반응형

1. URL이란?

 URL은 Uniform Resource Location의 약자로, 브라우저가 인터넷상의 정보를 찾는데 필요한 리소스(Resource) 위치(Location)를 말하며, 이 정보는 정형화(Uniform)되어 있다. 즉, 문법이 존재한다.

 


 

2. URL 문법

 URL은 일반적으로 9개 컴포넌트로 구성된다. 이 중 가장 중요한 컴포넌트 세가지는 스킴, 호스트, 경로이다.

<스킴>://<사용자 이름>:<비밀번호>@<호스트>:<포트>/<경로>;<파라미터>?<질의>#<프래그먼트>

 

컴포넌트 설명
스킴 리소스를 가져오기위해 사용할 프로토콜을 말한다. (ex. http, https)
사용자 이름 몇몇 스킴은 리소스에 접근하기 위해 사용자 이름을 필요로 한다.
비밀번호 사용자 이름에 대한 비밀번호를 가리킨다.
호스트 리소스를 호스팅하는 서버의 호스트 명이나 IP 주소이다.
포트 리소스를 호스팅하는 서버가 열어놓은 포트번호로 많은 스킴이 기본 포트를 갖는다. (ex. http는 80 포트)
경로 서버 내 리소스가 어디에 있는지를 가리킨다.
파라미터 입력 파라미터를 기술하는 용도로 사용된다. 매트릭스 파라미터 방식이 사용되나, JAX-RS라는 프레임워크만 지원되어 잘 사용되지 않는다.
질의 파라미터를 전달하는데 쓰인다. 이게 우리가 흔히 알고있는 쿼리 스트링이다.
프래그먼트 리소스의 특정부분을 가리키는 이름이며, 리소스 로드 후 특정 부분으로 스크롤을 이동시킨다. 서버에 전달되지 않으며 클라이언트에서만 사용한다. 

 


 

3. URL의 문자제한

 일반적으로 URL에는 ASCII 문자만 포함하도록 허락했다. 하지만 현대의 웹은 다양한 언어와 문자를 지원하기 위해 URL에 비-ASCII 문자도 사용할 수 있게 되었다. 인코딩 + 이스케이프 기능을 사용함으로써 말이다.

 예를들어 한글 '안'의 경우 ASCII 문자가 아니다. '안'을 UTF-8로 인코딩하면 'EC 95 88'이다. 이에 이스케이프 기능을 적용하면 % 기호로 시작하는 ASCII 문자열로 표현되며, 최종적으로 '%EC%95%88'로 변환된다.

 

반응형

+ Recent posts