반응형

ITEM 1 "생성자 대신 정적 팩토리 메서드를 고려하라"

 

 

이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 1편 해설 영상을 보는 것을 추천한다.

열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.

 

이번 장에서는 생성자 대신 정적 팩토리 메서드를 사용하는 것을 권장하고 있다.

 

public class Point {
	double x, y;
    
    // 생성자
    public Point(double x, double y) {
    	this.x = x;
        this.y = y;
    }
    
    // 정적 팩토리 메서드
    public static Point asPolar(double rho, double phi) {
    	double x = rho * Math.cos(phi);
        double y = rho * Math.sin(phi);
        return new Point(x, y);
    }
    
    // 이렇게 생성할 수 없음. 위 생성자와 타입과 인자의 갯수가 같으므로
    public static Point(double rho, double phi) {
    	this.x = rho * Math.cos(phi);
        this.y = rho * Math.sin(phi);
    }
}

 

여기 위와 같이 직교좌표나 극좌표에서 하나의 점을 표시하는 클래스가 있다.

클래스 이름과 같은 함수를 생성자라고 하고, "public static" 키워드가 붙고 클래스 객체를 생성하는 함수를 정적 팩터리 메서드라고 한다. 생김새만 다를 뿐 생성자와 같은 역할을 하는데 정적 팩터리 메서드를 권장하고 있다.

 

정적 팩터리 메서드는 아래와 같은 장단점을 가지고 있다.

 

장점 1 이름을 가질 수 있다.

생성자는 클래스 이름과 동일하게 함수 이름을 작성해야 한다는 규칙이 있다. 그래서 매개변수와 생성자의 이름만으로 반환될 객체의 특성을 제대로 설명하지 못 한다는 특징이 있다. 하지만 정적 팩토리 메서드는 위 예제와 같이 극좌표계에서 쓰일 객체를 "asPolar" 라는 이름으로 잘 설명해주고 있다.

 

또 타입의 순서와 갯수가 같은 동일한 생성자를 만들 수 없다. 자바에서는 오버로딩을 지원하고 있지만 어디까지나 매개변수 타입의 순서와 갯수가 같아야 한다. 위 코드에서 극좌표계 생성자를 만들다가 실패한 코드가 그 예제이다. 

 

장점 2 새로운 객체를 생성할 필요는 없다.

생성자는 이름 그대로 반드시 하나의 객체를 생성해야 한다. 그렇지만 정적 팩토리 메서드는 프로그래머가 임의로 작성한 메서드이기 때문에 제약이 없다. 불변한 객체를 미리 만들어 놓고 그 객체를 재사용하는 식으로 코드를 작성할 수 있다. 객체가 불변함이 보장된다면 굳이 하나의 객체를 생성해야 하는 생성자는 메모리 비용이 많이 드는 나쁜 선택이다.

 

public class Point {
	
    private static Point pointInstance;
	private static final Point SINGLE_POINT_OBJECT = new Point();

    public static Point getPoint() {
        return SINGLE_POINT_OBJECT;
    }

    public static Point getPointWithSingleTon() {
        if (pointInstance == null) {
        	pointInstance = new Point();
        }
        return pointInstance;
    }
}

 

이렇게 객체 생성을 컨트롤 할 수 있어 인스턴스 통제 클래스라고 부르며 언제, 어디까지 인스턴스를 살게 할 수 있을지 결정할 수 있다. 싱글톤 패턴을 만들 때에도 유용하고 불변 클래스를 만들 때에도 사용된다.

 

장점 3 반환 타입의 하위 타입 객체를 반환할 수 있다.

말 그대로 상속받은 자식 클래스의 객체를 반환할 수 있다는 뜻이다. 자식 클래스의 객체를 반환할 수 있게 해준다면 그 자식 클래스를 공개하지 않고도 객체를 사용할 수 있다는 것이다. 명시한 인터페이스만 다른 시스템에 노출시키고 구현체는 숨김으로써 객체지향의 OCP 원칙을 잘 지키며 설계할 수 있다. 이를 인터페이스 기반 프레임워크라고 부른다.

 

다른 시스템에 노출 시키려면 모두 public 으로 제한자를 풀어야 되는데 구현체는 private 로 숨기고 API 만 public 으로 만들고 그 인터페이스 정적 팩토리 메서드에서 구현체들을 반환만 해주는 것이다.

 

자바 8 이전에는 인터페이스에서 정적 메서드를 만들 수가 없었다고 한다. 그래서 인스턴스화가 불가한 동반 클래스로 만들어 그 안에 정의했다고 한다.

 

장점 4 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

장점 3의 연장선 상의 개념이다. 조건에 따라 하위 타입의 객체를 반환할 수 있게 설계가 가능하다.

이 책에서는 EnumSet 클래스를 예로 들며 이 클래스는 public 생성자가 없고 원소의 갯수에 따라 객체를 반환하는 메서드만 있다고 한다. 원소가 64개 이하면 RegularEnumSet 객체를, 65개 이상이라면 JumboEnumSet 객체를 반환한다.

Intellij 에서 직접 확인해봤다.

 

 

맨 마지막 EnumSet 생성자 보면 접근 제한자가 없다. 같은 패키지 내에서만 호출될 수 있다.

 

 

그리고, noneOf 이라는 정적 팩토리 메서드에서 원소의 개수에 따라 하위 클래스의 객체를 반환하는 것을 볼 수 있다.

이 클래스를 사용하는 프로그래머 입장에서는 저런 세세한 구현을 알지 못하더라도 공통적인 기능의 EnumSet 을 사용할 수 있게 된다. 장점 3에서 이야기 하였듯이 실제 구현은 공개하지 않았는데도 명세서만 보고 개발이 가능하다.

 

장점 5 정적 팩토리 메서드를 작성할 때 반환할 객체의 클래스가 존재하지 않아도 된다.

책에서는 서비스 프로바이더 프레임워크를 예시로 들면서 설명하고 있는데 그 개념을 모르는 사람은 어렵기도 하고 이해가 가지 않을 수 있다. 쉽게 설명하면 "구현""정의" 그리고 "사용" 관점에서 나누어 설계되어 있는 프레임워크라고 보면 된다. 정의부는 이 시스템에서 제공하고자 하는 명세서를 작성하는 담당을 하고, 구현부는 실제 그 기능을 만드는 구현체이다. 사용부는 사용자가 이 시스템에 접근해서 서비스를 요청하는 담당을 한다. 여기서 사용부는 어떻게 구현되었는지 알 필요가 없다. 무엇을 요청할지만 알면 된다. 그래서 사용부의 API 는 정의 인터페이스만 반환할 뿐이다.

정의 인터페이스와 연결만 되어 있으니 반환할 객체의 클래스가 존재하지 않아도 개발이 가능하다.

 

단점 1 상속이 불가능하다. == 하위 클래스를 만들지 못 한다.

상속을 하려면 public 이나 protected 생성자가 필요한데 정적 팩토리 메서드가 구현된 클래스는 생성자가 불필요하기 때문에 private 으로 보통 선언한다. 따라서 상속이 불가능하며 하위 클래스를 만들지 못한다.

나중에 "상속보다 합성", "불변 클래스" 라는 말을 많이 듣게 될텐데 상속이 불가능하다는 것은 단점이라기 보다는 장점으로 보일 수 있다.

 

단점 2 프로그래머가 정적 팩터리 메서드를 찾기가 어렵다.

단점이라기 보다는 불친절에 가깝다. 자바독을 보면, 생성자는 설명을 잘 해두지만 정적 팩터리 메서드는 직접 개발자가 소스코드를 뒤지며 찾아야 한다. 안 써있다. 앞으로 개발자들이 생성자와 같은 역할을 하는 정적 팩터리 메서드도 잘 문서화하면 문제가 없을 것이다. 그 이전에 작성해두었던 정적 팩토리 메서드는 어떻게 찾아야 될까?

흔히 사용되는 정적 팩터리 메서드 명명 방식들을 눈에 익혀두고 생각이 안 나면 이 접두사들을 보고 찾자.

 

from 매개변수가 하나일 때 사용
of 여러 매개변수를 받을 때 사용
valueOf from 과 of 의 더 자세한 버전
getInstance / instance 매개변수로 명시한 인스턴스 반환
newInstance / create getInstance/instance 와 기능은 같지만, 매번 새로운 인스턴스를 생성해서 반환
getType 여기서 Type 은 다른 클래스 이름. 다른 클래스의 객체를 반환
newType newInstance 와 같지만, 다른 클래스의 객체를 반환
type getType 과 newType 의 간결한 버전

 

"정적 팩토리 메서드를 고려하고 항상 문서화하자."

반응형
반응형

객체지향의 사실과 오해

 

"객체지향의 사실과 오해" 책의 2부 "이상한 나라의 객체" 부문 리뷰이다.

 

1부까지는 객체지향이 실세계와 다르며 자율성을 가진 객체들의 협력 시스템이라는 점을 강조했다.

객체들에게 책임감을 부여하고 간섭을 하지 않으려면, 명확한 경계가 필요하다. 어떻게 객체들을 나누어야 하며 객체란 무엇인지 확실히 정의를 해두어야 설계할 때 혼란이 없다. 그렇다면 정의는 과연 무엇일까? 필자도 여러 방면으로 생각을 해보았지만, 가장 근사한 대답은 "일반적인 특징들의 집합" 이 정의라고 생각했다. (주의할 점은 성급한 일반화의 오류는 피해야 한다.)

 

그래서 이 책에서는 일단 객체들의 특징들을 쉽게 설명하기 위해 '이상한 나라의 엘리스' 동화에 나오는 엘리스를 객체로 보고 특징들을 찾아내기 시작했다. 엘리스가 모험하는 과정에서 키가 작아지고 커져야 비로소 다른 세계를 갈 수 있는데 이런 행동들은 특징점이 있다. 키라는 상태에 따라 행동을 할 수 있고 엘리스의 행동에 의해 위치라는 상태가 바뀐다. 즉 다시 말해, 행동과 상태가 서로를 참조해서 다음 행동들을 결정 짓는다.

 

게임 프로그래밍 패턴 중 "상태 패턴" 이 있는데 이와 개념이 유사하다. 행동을 했는지 여부를 조건으로 다음 행동을 결정지으면 조건을 삼은 행동이 어떤 행동에 의해 유발되었는지 모두 체크해야 하므로 아래와 같이 복잡한 구조를 띄게 된다.

 

행동의 여부로 검사를 하게 된다면...

 

F 라는 행동 다음에 G 라는 행동을 하려고 한다고 가정하자.

그럼 F 라는 행동이 어떤 행동들로 인해 순차적으로 실행이 되었는지 관련 이력들을 모두 찾아봐야 한다. A 행동으로 부터 온 것인지, C 행동으로 부터 온 것인지 말이다. 예를 들어 F 가 "B 라는 버튼을 누른다" 행동이고 G 가 "플레이어가 점프" 하는 행동이라고 해보자. 게임이 무리없이 진행이 될 것 같아 보인다. 하지만 "이 플레이어는 점프가 가능하게 만들자" 라는 요구사항이 새롭게 요구되면 지금 점프 중인지 체크를 하는 코드를 삽입할 것이다.

 

행동은 무한하며 과거의 행동들에 대한 이력들을 모두 점검해야 된다. 그러나 상태는 유한하다. (FSM)

상태를 정의하고 상태에 따라 프로그래밍한다면, 더 손쉽게 개발할 수 있게 된다.

친절하게도 객체지향의 사실과 오해라는 책에서는 객체를 정의할 때 위와 같은 특징들을 놓치지 않고 적어두었다.

 

  • 객체는 상태를 가지며 상태는 변경 가능하다.
  • 객체의 상태를 변경시키는 것은 객체의 행동이다. 행동의 결과는 상태에 의존적이다.
  • 객체는 어떤 상태에 있더라도 유일하게 식별 가능해야 한다.

 

객체의 정의가 무엇인지 파악하기 위해 위와 같이 특징들을 추출했다. 그렇다면 상태는 무엇일까?

 

상태는 객체를 표현하는 일반적인 값일 수도 있고 다른 객체일 수도 있다. 객체지향 세계는 객체가 연결되어 있는 세계이기 때문에 다른 객체들을 가지고 있을 수 있다. 하나의 독립적인 자율개체이지만 어떤 객체의 상태를 표현하는 값이 될 수도 있다. 일반적인 값들은 정적이며 프로퍼티라고 하며 객체 안에 포함된 객체들은 동적이며 프로퍼티 값이라고 한다. 객체와 객체 사이에 연결이 되어 있어야만 하나의 상태로 표현될 수 있으며 언제든지 링크를 끊을 수 있다는 특징도 있다.

 

상태는 어떤 객체가 가지고 있는 정보의 집합으로써 정적인 프로퍼티와 동적인 프로퍼티 값으로 이루어진다는 것이다.

 

상태를 설명하며 자연스레 행동의 특징이 설명되었다. 행동은 자신의 상태를 변경하는 일련의 작업이다.

그런데 객체는 내부에 다른 객체를 가지고 있을 수도 있다. 직접 내부 객체의 상태를 바꿀 수 없으므로 내부 객체에게 메세지를 전달해야 한다. 개념을 합치면 행동은 자신의 상태를 변경하는 작업 + 내부 객체에게 메세지를 보내는 작업이다.

 

폐쇄적인 세계이다보니 이상한 특징들이 있다. 내부 객체가 제공하는 행동만 메세지를 보낼 수 있다는 점이다.

그 내부 객체가 행동을 제공하지 않으면 연결 고리가 없는 셈이며 이는 곧 고립이 된다. 그래서 상태부터 고려하지 말고 행동부터 정의하고 그 행동들을 유한 상태로 만들 수 있는 상태들을 정의해야 한다라고 한다.

 

이를 책임 주도 설계. (Responsibility-Driven Design) 라고 하고 행위가 우선이 되어야 한다라는 점을 나중에 강조하기 위해 복선을 깔아둔다.

 

2부에서는 객체의 정의가 무엇인지 알 수 있었다. 하지만 아직 특징만 파악했을 뿐 어떻게 나누어야 하는지 알 수 없다.

다음 부에서 설명하게 된다.

반응형
반응형

오늘의 공부는 "어디 가서 코프링 매우 알은 체하기"입니다.

원본 영상은 아래 링크를 참조해주세요~

 

 

떠오르는 언어(?) 코틀린으로 스프링 웹 백엔드를 구현해보고자는 열망이 있었던 찰나에 코틀린과 스프링에 대해서 공부할 수 있는 영상이 있어서 공부한 내용을 적어보고자 한다. 

 

아이템 1. 코틀린 표준 라이브러리를 익히고 사용하라.

 

"표준 라이브러리를 사용하면 그 코드를 작성한 전문가의 지식과 여려분보다 앞서 사용한 다른 프로그래머들의 경험을 활용할 수 있다." 라는 명언을 통해 자바 코드 대신 코틀린 표준 라이브러리를 사용하라고 적극 권장하고 있다.

코틀린에는 읽기 전용 컬렉션과 변경 가능한 컬렉션을 구분해서 제공한다. 플랫폼에서 사용하는 컬렉션들이 읽기/쓰기 전용에 맞게 구분되어 제공되어진다. 코틀린에는 좋은 기능들이 많다. 애용하자.

자바와 관련된 코드들을 줄여나가다 보면 자연스럽게 코틀린스러운 코드를 작성할 수 있게 된다. 

 

아이템 2. 자바로 역컴파일하는 습관을 들여라.

 

코틀린 숙련도를 향상시키는 가장 좋은 방법 중 하나는 작성한 코드가 자바로 어떻게 표현되는지 보는 것이다.

역컴파일을 통해 예기치 않은 코드를 방지할 수 있고 문제가 발생했을 때 빠르게 확인할 수 있다.

IntelliJ IDEA 에서 Tools -> Kotlin -> Show Kotlin Bytecode -> Decompile 기능을 통해 역컴파일할 수 있다.

 

아이템 3. 롬북 대신 데이터 클래스를 사용하라.

코틀린과 자바를 혼용하여 사용하면 코틀린 먼저 class 파일로 변환이 되고 java 파일들을 읽어 애너테이션 프로세싱을 거치게 된다. 즉 애너테이션 프로세서는 코틀린 컴파일 이후에 동작하기 때문에 롬북에서 생성된 자바 코드는 코틀린 코드에서 접근할 수 없다. 그래서 코틀린 코드에서는 롬북 코드를 사용하지 못 한다(?) 라는 이야기가 나오게 되었다.

 

자바와의 호환성을 제공하고자 코틀린 진영에서도 롬북을 사용할 수 있게 1.5.20 부터 롬북 컴파일러 플러그인이 실험적으로 추가되기도 했다. 코틀린 코드보다 자바 코드를 먼저 컴파일하도록 빌드 순서를 조정하면 롬북 문제는 해결될 수도 있다. 하지만 이 역시 자바 코드에서 코틀린 코드를 호출할 수 없게 되는 단점이 생긴다. lombok 을 delombok 으로 순수 자바 코드로 풀어버리는 방법이 있지만, 코틀린의 data class 를 사용하는 것을 추천한다.

 

data class 를 사용하면 컴파일러가 알아서 equals, hashCode, toString, copy 함수등을 자동으로 생성해준다.

주 생성자에 있는 매개변수를 기반으로 생성되므로 하나 이상의 매개변수가 있어야 하며 모든 매개변수는 val 이나 var 로 표시해야 한다. copy 함수를 적절히 사용하면 데이터 클래스를 불변으로 관리할 수도 있다.

 

TIP. all-open 컴파일러 플러그인을 사용하라.

 

 

코틀린의 폐쇄적인 클래스 정책 때문에 SpringBoot main 함수가 실행이 되지 않는다. 자바로 컴파일해보면 Application 클래스에 final 이라는 키워드가 붙어 있다. @SpringBootApplication 은 @Configuration 을 포함하고 있는데 스프링은 기본적으로 CGLIB 을 사용하여 @Configuration 클래스에 대한 프록시를 생성한다.

 

CGLIB 에서 대상 클래스를 상속해서 프록시를 구현하는 문제 때문에 코틀린의 폐쇄적인 클래스 정책과 충돌이 나는 것이다. final 로 지정된 클래스와 메서드들은 상속하거나 오버라이드를 할 수 없기 때문이다.

(스프링 5.2부터 @Configuration 의 proxyBeanMethod 옵션을 사용하여 프록시 생성을 비활성화하는 기능을 사용하면

동작이 된다고 한다.)

 

상속을 허용하고 오버라이드를 허용하려면 open 이라는 변경자를 추가해야 한다. 그런데 spring 에 관련된 모든 클래스와 메서드에 open 이라는 지시자를 붙여야 한다면 코틀린의 매력도가 떨어지게 된다.

 

 

코틀린에서는 이러한 문제점을 해결하고자 all-open 플러그인을 제공한다. all-open 플러그인을 사용하면 지정한 애너테이션이 있는 클래스와 모든 멤버에 open 변경자를 추가한다. 스프링을 사용할 경우 이 all-open 플러그인을 랩핑한 kotlin-spring 컴파일러 플러그인을 사용하면 된다. @Component, @Transactional, @Async 등이 기본적으로 제공된다.

적용되는 애너테이션 항목들을 확인하려면 Intellij IDEA 에서 File -> Project Structure -> Project Settings -> Modules -> Kotlin Compiler Plugins 에서 확인하면 된다.

 

아이템 4. 필드 주입이 필요하면 지연 초기화를 사용하라.

 

생성자를 통해 의존성을 주입하는 것이 당연시 되는 오늘이지만, 때로는 필드를 통해 주입해야 될 때가 있다.

이 때 코틀린에서는 backing field 가 존재하는 프로퍼티라면 인스턴스화될 때 초기화해야 된다. null 로 초기화 할 수는 있지만 null 이 있는 타입은 항상 null 연산자를 통해 체크로직이 필요하다는 불편함이 있다. 코틀린에서는 lateinit 변경자를 붙이면 프로퍼티를 나중에 초기화할 수 있다.

 

TIP. 역직렬화를 위해 잭슨 모듈이 코틀린을 지원하고 있다.

 

역직렬화를 하려면 매개변수가 없는 생성자가 필요하다. 그런데 코틀린에서는 매개변수가 없는 생성자를 만드려면 모든 매개변수에 기본 인자를 넣어야 한다. 잭슨 코튤린 모듈은 매개변수가 없는 생성자가 없더라도 직렬화와 역직렬화를 지원한다. 스프링 부트에서 기본적으로 이 모듈을 포함하고 있다고 한다.

 

아이템 5. 변경 가능성을 제한하라.

 

폐쇄적으로 작성할 수록 안전하기 때문에 모든 멤버를 val 로 선언할 것을 권장하고 있다. (나중에 var로 바꾸면 되므로!) 스프링 부트 2.2 부터 스프링 프로퍼티 클래스에서 생성자 바인딩이 가능하다. (@EnableConfigurationProperties 또는 @ConfigurationPropertiesScan 사용) Configuration 클래스에 적극 활용하자.

클래스에 개념적으로 동일하지만 하나는 공개하고 다른 하나는 구현 세부사항으로 구현할 때 private 프로퍼티 이름의 접두사로 밑줄을 사용한다. 이를 뒷받침하는 프로퍼티(backing property) 라고 한다. 공개되는 쪽에는 읽기 전용의 자료구조를 사용하고 내부에서 사용할 때는 수정이 가능한 자료구조를 사용한다. JVM 에서 기본 getter 와 setter 가 있는 private 프로퍼티에 대해서 함수 호출 오버헤드를 방지하도록 최적화되어 있다.

 

TIP. No-arg 컴파일러 플러그인

 

역직렬화와 마찬가지로 엔티티 클래스를 생성하려면 매개변수가 없는 생성자가 필요하다. no-arg 컴파일러 플러그인은 지정한 애너테이션이 있는 클래스에 매개변수가 없는 생성자를 추가한다. 자바 또는 코틀린에서 직접 호출할 수는 없지만 리플렉션을 사용하여 호출할 수 있다. no-arg 컴파일러 플러그인을 랩핑한 kotlin-jpa 컴파일러 플러그인을 사용할 수 있다. 마찬가지로 스프링 부트에서 자동으로 셋팅되며 @Entity, @Embeddable, @MappedSuperclass 등이 기본적으로 제공된다.

 

 

Q. kotlin("plugin.jpa")구문만 있으면 될 것 같은데 Entity와 MappedSuperclass에 allOpen 애너테이션이 붙은 이유는?

 

프록시를 못 만들기 때문이다. JPA 에서 프록시를 못 만든다는 뜻은 지연로딩을 할 수 없다는 뜻과 같다. 컴파일 에러나 런타임 에러는 발생하지 않는데 성능 문제가 발생하기 시작한다. 지금 당장 로딩이 필요없는 수많은 데이터들이 즉시 로딩되면서 성능 문제가 발생하는 것이다.

 

아이템 6. 엔티티에는 데이터 클래스 사용을 피하라.

 

lombok 의 @data 와 같은 맥락이다. 양방향 연관관계의 경우 toString, hashcode 를 호출할 때 무한 순환 참조가 발생한다.

 

아이템 7. 사용자 지정 getter를 사용하라.

 

JPA 에 의해 인스턴스화 될 때 초기화 블록이 호출되지 않기 때문에 영속화하지 않는 필드는 초기화된 프로퍼티가 아닌 사용자 지정 getter 를 사용해야 한다.

 

 

왼쪽 코드처럼 영속화되지 않는 필드에 초기화 구문을 넣어도 데이터베이스에서 값을 가져올 때 boolean 이나 null 이 될 수 없음에도 호출해보면 null 이 들어가 있다. 오른쪽 코드처럼 getter 를 사용자 정의해서 프로퍼티에 접근할 때마다 호출할 수 있게 구성하여야 한다. 뒷받침하는 필드가 존재하지 않기 때문에 AccessType.FIELD 이더라도 @Transient 를 사용하지 않아도 된다.

 

아이템 8. 널이 될 수 있는 타입은 빠르게 제거하라.

 

null이 될 수 있는 타입을 사용하면 null 검사를 넣거나 !! 연산자를 통해 assert 검사를 해야 한다.

0 또는 빈 문자열로 초기화하면 null이 될 수 있는 타입을 제거할 수 있다. 확장 함수를 사용해 반복되는 null 검사를 제거할 수도 있다. 의문이 드는 건 0 또는 빈 문자열로 초기화해도 검사해야 되지 않나이다. 자연스레 0 이나 빈 문자열을 두어도 상관이 없는 괜찮은 코드라면 무방할 수도 있겠다. 확장함수를 사용해 반복되는 null 검사를 제거할 수도 있다.

반응형

'언어 > Kotlin' 카테고리의 다른 글

[Kotlin] 코틀린 특징  (0) 2022.04.16
[Kotlin] 코틀린의 코루틴 동작 원리  (0) 2022.04.12
반응형

코틀린 언어가 가지고 있는 다른 언어와 다른 특징

 

#1 코틀린은 기본적으로 상속이 불가능하다.

 

 

코틀린 공식 홈페이지에 가보면 코틀린 클래스는 final 클래스이며 상속이 불가능하다고 명시되어 있다.

상속을 허용하려면 open 이라는 지시어를 써야 한다. 이와 관련해서 Github 에서 코틀린 스프링에 관련한 예제를 서치하다가 아래와 같은 내용을 발견했다.

 

출처 : https://github.com/arawn/kotlin-spring-example

 

지금은 공식 홈페이지에 설계 원칙에 대한 내용은 찾아 볼 수 없지만 위 github 작성 당시에는 설계 원칙이 있었던 것 같다. 코틀린은 이펙티브 자바 Item 17 "상속을 허용하지 말라" 를 철저히 지킬 수 있도록 설계된 것 같다.

 

상속 시 주의할 점은 다음과 같다.

- 서브 클래스는 수퍼 클래스에 존재하는 속성과 같은 이름의 속성을 가질 없다. 그래서 변수들을 var 이나 val 선언하지 말고 파라미터 넘겨서 클래스 생성자에 직접 넣어주어야 한다.

- 서브 클래스는 수퍼 클래스의 생성자까지 호출되어야 한다. 기본 생성자이던 보조 생성자이던 반드시 수퍼 클래스의 생성자가 불려야 한다.

 

#2 코틀린은 기본적으로 메서드 오버라이딩, 속성 재정의가 불가능하다.

 

1번과 마찬가지로 변경을 최소화하기 위해 클래스 내에 선언되어 있는 메서드들도 오버라이딩이 불가능하다.

수퍼 클래스에서 open 이라는 키워드로 오버라이딩이 가능하게 만들어 주어야 서브 클래스에서 override 라는 지시자를 통해 오버라이딩이 가능하다. 속성도 마찬가지이다.

 

 

#3 코틀린은 암시적 형변환은 지원하지 않는다.

 

// 컴파일 에러 Type Mismatch

var a: Int = 123
var b: Long = a

암시적 형변환으로 인한 오류를 줄이고자 명시적 형변환만 허용하고 있다. to자료형 메서드를 사용해서 명시적으로 변환해야한다.

 

#4 코틀린은 타입 추론이 가능하다.

 

코틀린은 변수나 함수를 선언할 때나 연산이 이루어질 때 자료형을 코드에 명시하지 않아도 자동으로 자료형을 추론해준다. 기본 자료형들은 값을 선언시 할당만 해주면 알아서 추론이 가능하다. 단일 표현식 함수에서도 반환형의 타입추론이 가능하다.

 

fun add(a: Int, b: Int, c: Int) = a + b + c

 

#5 코틀린은 기본 변수에서 null 절대 허용하지 않는다.

 

Null pointer Exception Error 를 원천적으로 차단하기 위해서 Null 을 할당할 수도 사용할 수도 없다.

보통 다른 언어에서는 변수 선언 시 초기화를 하지 않으면 기본값으로 초기화되거나 값이 할당되지 않았다는 표시로 null 값을 가지게 되는데 코틀린은 문법 에러를 표시하고 컴파일이 불가능하다.

그렇다고 꼭 선언 시에 초기화할 필요는 없다. lateinit 이나 lazy 변수를 활용하여 지연 초기화를 사용해도 된다.

lateinit 변수는 초기화 할당 전까지 변수를 사용할 수 없고 클래스 내부에서만 사용이 가능하다. 초기화를 하였는지 여부는 ::var.isInitialized 키워드를 사용해서 확인한다.

lazy 변수는 변수를 사용하는 시점까지 초기화를 자동으로 늦춰주는 기능이다. val a: Int by lazy {람다함수} 구문을 통해 설정할 수 있다.

 

그런데 정말 null 이 필요한 경우가 생길 수 있으므로 자료형 뒤에 var a:Int? 물음표를 붙이면 null 을 허용하는 nullable 변수로 선언해줄 수도 있다. null 상태로 속성이나 함수를 사용하려고 하면 null pointer exception 이 발생할 우려가 있기 때문에 null 체크 없이는 코드가 컴파일 되지 않는다. 이 때 null 체크를 위해 일일히 if 문으로 조건을 체크하는 대신 아래 연산자를 통해 간단히 체크할 수 있다.

 

 

null safe operator 의 경우에는 참조연산자를 실행하기 전에 먼저 객체가 null 인지 확인부터하고 null 이라면 뒤 따라오는 구문을 실행하지 않는다.

elvis operator 는 객체가 null 이 아니라면 그대로 사용하지만 null 이라면 우측 피연산자 값으로 대체된다.

non-null assertion operator 라면 참조연산자를 사용할 때 null 여부를 컴파일 시 확인하지 않도록 하여 런타임 시 null pointer exception 이 발생하도록 의도적으로 방치하는 연산자이다.

 

보통 위 연산자들과 스코프 함수를 같이 실행한다.

 

var a: String? = null

a?.run{
	println(toUpperCase())
    println(toLowerCase())
}

 

#6 코틀린은 읽기만 가능하고 수정이 불가능한 컬렉션과 수정 및 삭제까지 가능한 Mutable 컬렉션을 제공한다.

반응형

'언어 > Kotlin' 카테고리의 다른 글

[코프링] 코틀린과 스프링 (1)  (0) 2022.04.16
[Kotlin] 코틀린의 코루틴 동작 원리  (0) 2022.04.12
반응형

오늘의 공부는 코틀린의 코루틴 동작원리입니다.

원본 영상은 아래 링크를 참조해주세요.

 

 

코틀린의 코루틴들은 사실 컴파일러에서 Finite State Machine 기반의 재귀 함수로 변환된다.

 

콜백 지옥

 

비동기 코드들이 일반적으로 중첩 for 문의 callback 코드 조각들처럼 구성되어 있는데 코틀린에서는 다르다.

코틀린 컴파일러는 코드 조각들을 순차적으로 작성해도 컴파일 타임 시에 코드 조각들을 찢어 state 재귀 함수로 변환해준다. 상태와 결과 체크를 컴파일러에서 해주기 때문에 callback 지옥에서 벗어나 프로그래머가 순차적으로 작성할 있게 도와준다.

이러한 과정을 "코틀린 컴파일러가 suspend 붙은 함수에 대해 Continuation Passing Style(CPS) 바꾼다" 라고 표현한다.

 

 

위와 같은 예제의 코드를 비동기적으로 처리한다고 가정하자.

 

 

코틀린에서 자바로 변환될 번째 줄부터 Continuation 객체가 주입(Dependency Injection) 것을 있다.

 

 

Continuation 인터페이스는 현재 상태의 context resume 메서드를 가지고 있다.

콜백 지옥에서도 자세히 보면 callback 함수들을 전달하는 것을 있는데 코틀린에서는 callback 함수들을 인터페이스화하여 추상화하고 인자로 주입한 것이다.

 

 

먼저 라인 별로 Label 지정해 자르고,

 

 

앞에서 소개한 Continuation 객체를 인자로 주입하고, 코루틴 구현체를 만들어 Continuation 객체를 체크해서 재귀함수를 호출할지 결정한다.

 

 

case 따라 다음 case 필요한 인자를 셋팅하고 순차적으로 호출하는 것을 있다.

 

코틀린 개발자들은 겸손해서 그런지 Magic 이 아니라고 하는데

나는 다중 for 문(?)의 구조를 상태 DI(Dependency Injection) 와 Finite State Machine 으로 구현한 것이 인상적이었다

반응형

'언어 > Kotlin' 카테고리의 다른 글

[코프링] 코틀린과 스프링 (1)  (0) 2022.04.16
[Kotlin] 코틀린 특징  (0) 2022.04.16
반응형

오늘의 공부는 백기선님의 @OneToMany 양방향 관계 쿼리 문제입니다.

원본 영상은 아래 링크를 참조해주세요~

 

 

@Entity
@Getter @Setter
public class Book {
	@Id @GeneratedValue
    private Integer id;
    
    private String isbn;
    
    private String Title;
    
    @ManyToOne
    private BookStore bookstore;
}

@Entity
@Getter @Setter
public class BookStore {
	@Id @GeneratedValue
    private Integer id;
    
    private String name;
    
    @OneToMany(mappedBy="bookStore")
    private Set<Book> books = new HashSet<>();
    
    void add(Book book) {
    	this.books.add(book);
    }
}

@Test
public void contextLoads() {
	BookStore bookStore = new BookStore();
    bookstore.setName("시애틀 책방");
    bookStoreRepository.save(bookStore);
    
    Book book = new Book();
    book.setTitle("JAP 공부 좀 하면서 쓰세요");
    
    bookStore.add(book);
    bookRepository.save(book);
}

책 테이블에 연관관계가 설정되지 않은 모습

Q . 왜 책 테이블에 연관관계가 설정되지 않았는가?

양방향 관계의 기본 문제이다. 테이블과 객체는 본질적으로 다르다.

테이블은 foreign key 가 어디 있던지 각 테이블에서 join 에서 조회할 수 있지만, 객체는 알 수 없다.

그래서 양방향 관계 설정 시 연관관계의 주인이 누구인지(FK 키를 누가 가지고 있는지) 알려주어야 한다.

 

mappedBy 속성으로 상대편 테이블이 주인이라는 것을 알려준다.

관계의 주인인 쪽에서 관계가 설정이 되어야 한다. 그래야 데이터베이스에 제대로 반영이 된다.

 

그래서 public void add(Book book) 메서드 내에 book.setBookStore(this); 를 삽입하면 문제 해결이 된다.

 

반대 코드인 getBooks().add(book) 도 객체 지향적인 관점에서 보았을 때 당연히 해주어야 한다.

데이터베이스에 적용할 때는 아무런 작용이 되지 않지만 객체에서도 값을 적용해주어야 하기 때문에 당연히 해야 한다.

반응형

'JAVA > JPA' 카테고리의 다른 글

[JPA][백기선] @ManyToOne 단방향 관계 쿼리 문제  (0) 2022.04.09
반응형

오늘의 공부는 백기선님의 @ManyToOne 단방향 관계 쿼리 문제입니다.

원본 영상은 아래 링크를 참조해주세요~

 

 

Q . 왜 테스트 코드에서는 select 쿼리가 한 개만 발생하고 Controller 코드에서는 3개가 발생하는가?

테스트 코드의 트랜잭션의 범위는 testSelect 함수이다.

@DataJpaTest 어노테이션이 @Transactional 어노테이션을 가지고 있는데 이는 public 메서드들에 트랜잭션 단위를 부여하는 것과 동일하다. 즉 testSelect 함수 전체가 하나의 트랜잭션이다.

트랜잭션 하나에 Persistence Context 를 가지고 있으므로 save 를 할 때마다 1차 cache 에 저장된다.

그러므로 1차 cache 에서 바로 꺼낼 수 있기 때문에 다시 select 할 필요가 없다.

 

그런데 Controller 코드에서는 findAll 하나만 트랜잭션 범위이다.

기본 전략이 EAGER 이므로 Team 정보도 같이 가져올 수 밖에 없다.

Q . Member 정보만 쿼리 한 번으로 가져오려면 어떻게 해야 하는가?

LAZY 라는 Fetch 옵션을 사용하는 방법을 생각해 볼 수 있다.

Fetch 전략은 처음 Member 라는 정보를 조회해올 때 Team 을 참조하는 코드가 있지 않는 이상 Team 에 대한 데이터를 조회해오지 않는다. Team 에 대한 정보를 조회하는 순간이 오면 그 때 Team 에 대한 select 쿼리가 발생한다.

 

그러나, findAll 메서드는 객체를 JSON 형태로 변환해서 전달해 주는데 프록시인 객체를 JSON 으로 변환하려고 하니까 오류가 발생한다. LAZY 옵션을 사용하면 Team 에 대한 레퍼런스가 ByteBuddyInterceptor 라는 프록시 객체로 변경되기 때문이다. DTO 를 사용해서 modelmapper 로 다시 맵핑하면 된다.

 

댓글 중에 interface 를 사용하는 방법이 있다고 한다. projection!

Q . Member 와 Team 정보를 쿼리 한 번으로 가져오려면 어떻게 해야 하는가?

JPQL 에서 fetch join 을 사용하면 간단하게 해결이 될 것 같은데... JPQL 을 사용하지 않은 것을 전제로 하는 것 같다.

 

(공부해보니...)

fetch join 은 다음과 같은 단점도 존재한다. 같은 테이블을 조회하는 경우지만 어떤 테이블을 같이 조회하느냐에 따라 중복되서 쿼리를 작성하기 때문에 비추천한다고 한다.

 

@NamedEntityGraph 를 사용해서 Join 쿼리를 유도한다.

엔티티 그래프 기능은 엔티티 조회 시점에 연관된 엔티티들을 함께 조회하는 기능이다.

@NamedEntityGraph(name="엔티티 그래프 이름", attributeNodes={@NamedAttributeNode("함께 조회할 속성")})

반응형

'JAVA > JPA' 카테고리의 다른 글

[JPA][백기선] @OneToMany 양방향 관계 쿼리 문제  (0) 2022.04.09
반응형

객체지향의 사실과 오해

 

"객체지향의 사실과 오해" 책의 1부 "협력하는 객체들의 공동체" 부문 리뷰이다.

 

"객체지향은 실세계를 모방하는 것이 아니다" 라는 신선한 주제를 던지면서 시작한다.

조금 거리감이 있음에도 불구하고 많은 사람들이 실세계를 모방하는 것이 객제지향이다라고 설명하는 것은 현대 사회와 객체지향이 "협력"이라는 관점에서 매우 비슷하기 때문이다. 각 객체들이 유기적으로 협력하며 자율적으로 행동하는 우리 사회의 모습을 초점을 두어 보면 틀린 말도 아니다.

 

그래서 이 책에서는 객체지향은 시스템을 자율성을 가진 객체들의 집합으로 바라보고, 책임들을 하나씩 부여해 분할하는 행위라고 규정지었다. 각각의 객체는 상태와 행위를 지니고 있으며 스스로 그 책임을 수행하기 위해 자율적으로 방법을 선택해야 한다고 한다. 그래서 그 자율성을 해치면 안 되기 때문에 외부에서 그 객체의 상태나 행위들을 지적하거나 간섭하면 안 된다고 서술하고 있다.

 

사람 간의 관계와 매우 유사하다고 생각했다. 사람은 스스로 생각할 수 있으며 스스로 행동한다. 우리 사회는 그런 사람들과 소통하며 협력하며 지낸다. 하지만 객체지향의 세계가 현실의 세계와 다른 점은 가르침을 받는 학생이나 가르침을 전수하는 선생님이 없다는 것이다. 모두가 어른이자 팀장이며 자율적으로 생각해서 처리하는 세계이고 요청과 응답만 있는 세계라는 것이다.

 

자신의 상태는 자신이 책임지어야 하며, 맡은 바 임무는 팀의 리더처럼 스스로 해야 한다는 것이다. 여러 개의 책임을 받아 하나의 역할로써 수행한다. 인사팀장, 기술팀장, 영업팀장처럼...

 

1장에서는 현실세계에 익숙한 사람들을 위해 현실세계에서 일어날 법한 커피 마시기 사례를 들어 객체지향의 중요한 개념들인 "역할", "책임", "협력" 등을 뽑아내어 각인시켰다.

반응형

+ Recent posts