05 Aug 2024
익명 구현 객체에서 사용하는 외부 변수는 final이어야 한다는데, 왜 그런지 알아보자.
익명 구현 객체
익명 구현 객체는 클래스나 인터페이스의 구체적인 구현을 직접 선언하지 않고, 인스턴스를 생성하면서 즉성에서 구현하는 방식이다.
보통 인터페이스나 추상 클래스를 구현할 때 사용되며, 특별한 이름을 갖지 않는 클래스 형태로 작성된다.
final 키워드
final 키워드는 변수, 메서드, 클래스에 사용될 수 있으며, 각기 다른 의미를 가진다.
변수에 final: 값이 한 번만 할당될 수 있다. -> 불변 상수
메서드에 final: 해당 메서드를 오버라이드 할 수 없게 된다.
클래스에 final: 해당 클래스를 상속할 수 없게 된다.
익명 구현 객체와 final
각 개념에 대해 정리했으니, 이 포스팅에 맞는 내용을 알아보자.
익명 클래스 내에서 외부 변수에 접근하려면, 그 외부 변수는 final이거나 사실상 final이어야 한다.
이는 해당 변수가 익명 클래스 내부에서 사용될 때 그 값이 변경되지 않도록 보장하기 위해서이다.
익명 클래스는 외부 클래스의 인스턴스와는 별도로 존재하는 내부 클래스이다.
익명 클래스가 생성될 때, 외부 변수의 값을 캡처하는 방식으로 저장하는데, 이때 중요한 점은 외부 변수의 변경 여부다.
자바는 익명 클래스에서 외부 변수를 사용할 때 해당 변수의 값을 클로저처럼 캡처한다.
왜 그러지? 그냥 참조해서 쓰면 되는거 아닌가?
단순히 참조해서 사용하지 않고 캡처를 해서 사용하는 것은 변수의 수명, 동작의 안정성 및 동시성 문제와 관련이 있다.
자바에서 익명 클래스나 람다 표현식은 클래스의 인스턴스를 동적으로 생성한다.
익명 클래스나 람다는 외부 메서드의 스택 프레임(즉, 메서드가 실행되는 동안만 존재하는 변수들)을 참조할 수 있다.
그럼에도 불구하고 메서드가 종료된 후에도 그 외부 변수를 익명 클래스가 참조할 수 있어야 한다.
이를 위해서는 외부 변수의 값이 메서드 종료 후에도 유지될 수 있어야 한다.
그렇기에 캡처는 이러한 변수를 힙영역에 저장하고, 익명 클래스가 이를 사용할 수 있도록 보장하는 방식이다.
이렇게 저장된 변수는 메서드 실행이 종료된 후에도 계속 존재하며, 익명 클래스나 람다가 실행될 때 참조할 수 있다.
또한 외부 변수를 캡처할 때 그 변수가 final이어야 하는 이유는 변경 불가능성을 보장하기 위해서다.
캡처한 값이 변경된다면 캡처된 값은 여전히 초기 값을 참조하고 있을 것이다.
이러한 상황에서는 동기화 문제나 값이 불일치되는 문제가 생긴다.
(final로 명시되지 않은 변수라도 값이 변경되지 않으면 사실상 final로 간주)
30 Jul 2024
영속성 컨텍스트란?
영속성 컨텍스트는 엔티티를 영구 저장하는 환경이라고 할 수 있다.
엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
엔티티 매니저를 통해서 영속성 컨텍스트에 접근할 수 있고 영속성 컨텍스트를 관리할 수 있다.
영속은 뭘까?
엔티티는 4가지의 상태가 존재한다.
- 비영속(new) : 영속성 컨텍스트와 전혀 관계가 없는 상태
- 영속(managed) : 영속성 컨텍스트에 저장된 상태
- 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(removed) : 삭제된 상태
즉, 비영속 상태의 엔티티를 영속성 컨텍스트에 저장한 상태를 영속이라고 한다.
영속성 컨텍스트는 왜 사용할까?
영속성 컨텍스트를 사용하면 이러한 장점들이 있다.
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지
- 지연 로딩
1차 캐시
영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이것을 1차 캐시라고 한다.
영속 상태의 엔티티는 모두 이곳에 저장된다.
엔티티를 지정할 때 @Id로 지정한 값이 식별자이고 값은 엔티티 인스턴스로 된 Map 형태로 저장된다.
예를 들어, em.find()를 호출하면 우선 1차 캐시에서 식별자 값으로 엔티티를 찾는다.
만약 찾는 엔티티가 있으면 데이터베이스를 조회하지 않고 메모리에 있는 1차 캐시에서 엔티티를 조회한다.
만약 1차 캐시에 엔티티가 없다면 엔티티 매니저는 데이터베이스를 조회해서 엔티티를 생성한다.
그리고 1차 캐시에 저장한 후 영속 상태의 엔티티를 반환한다.
동일성 보장
영속성 컨텍스트는 동일성을 보장한다.
예를 들어 em.find(Member.class, “member1”)를 반복해서 호출해도 영속성 컨텍스트는 1차 캐시에 있는 같은 엔티티 인스턴스를 반환한다.
트랜잭션을 지원하는 쓰기 지연
엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소에 SQL을 차곡차곡 모아둔다.
그리고 트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스에 보낸다.
변경 감지
JPA로 엔티티를 수정할 때는 단순히 엔티티를 조회해서 데이터만 변경하면 된다.
JPA는 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해둔다(스냅샷)
그리고 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다.
순서를 정리하면 다음과 같다.
- 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 플러시가 호출된다.
- 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
- 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다.
- 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
- 데이터베이스 트랜잭션을 커밋한다.
플러시
플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.
영속성 컨텍스트를 플러시하는 방법은 3가지가 있다.
- 직접 호출
- 엔티티 매니저의 flush() 메서드를 직접 호출해서 영속성 컨텍스트를 강제로 플러시한다.
- 트랜잭션 커밋 시 플러시 자동 호출
- 트랜잭션을 커밋하기 전에 플러시를 호출해서 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영해야 한다.
- 그렇기 때문에 JPA는 트랜잭션을 커밋할 때 플러시를 자동으로 호출한다.
- JPQL 쿼리 실행 시 플러시 자동 호출
- JPQL이나 CRITERIA 같은 객체지향 쿼리를 호출할 때도 플러시가 실행된다.
준영속이란?
영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 것을 준영속 상태라 한다.
따라서 준영속 상태는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.
detach(), clear(), close() 메서드는 엔티티를 준영속 상태로 만든다.
준영속 상태의 엔티티를 다시 영속 상태로 변경하려면 merge() 메서드를 사용하면 된다.
merge()는 파라미터로 넘어온 준영속 엔티티를 사용해서 새롭게 병합된 영속 상태의 엔티티를 반환한다.
파라미터로 넘어온 엔티티는 병합 후에도 준영속 상태로 남아있다.
25 Jul 2024
스프링에서 application.properties나 application.yml 파일에 설정된 값들을 가져오는 다양한 방법을 알아보자.
코드에 하드코딩하지 않고 properties 파일에 저장한 값을 읽어들이는 방법을 사용하는 이유는 다양하다.
- 환경에 따른 설정 분리
- 하드코딩된 값은 코드에서 수정해야 하기 때문에 환경마다 다르게 설정하려면 코드 자체를 수정해야 한다.
- properties 파일에 설정하고 profile 설정을 하면 개발, 테스트, 운영 환경 등 각 환경에 따라 다른 설정 파일을 만들어 관리할 수 있다.
- 보안 강화
- 중요한 정보(데이터베이스 비밀번호, AWS 키, 토큰 비밀번호 등)를 코드에 하드코딩하면 보안상 위험이 있다.
- 외부 설정 파일을 사용하면 .gitignore 에서 설정하여 형상관리에서 제거할 수도 있고 따로 파일을 관리할 수도 있다.
그러면 사용 방법들을 알아보자.
Environment
Environment 객체를 주입해서 getProperty 메서드로 properties 값을 가져올 수 있다.
@Value
@Value 어노테이션으로 properties 값을 가져올 수 있다.
@Value(“${프로퍼티}”) 이런식으로 ${}로 감싸는 형태로 작성한다.
@ConfigurationProperties
@ConfigurationProperties는 설정값을 객체 형태로 바인딩할 때 사용.
개인적으로 이 방법이 코드도 깔끔하다고 생각한다.
17 Jul 2024
모던 자바 인 액션을 공부하면서 만난 예제 코드를 연습겸 정리해보자.
모든 상점의 정보를 순차적으로 요청하는 메서드를 성능을 고려하여 개선하며 구현해보았다.
예시로 상점 리스트를 먼저 만들어보자.
단순 스트림
병렬 스트림
CompletableFuture로 비동기 호출
CompletableFuture를 이용하여 구현
비동기 처리를 했는데 만족할만한 결과가 아니다.
만약에 상점이 4개에서 5개로 늘어난다면 병렬 스트림보다 CompletableFuture가 아주 조금 빠르다.
CompletableFuture는 병렬 스트림으로 구현한 버전에 비해 작업에 이용할 수 있는 다양한 커스텀 Executor를 지정할 수 있다는 장점이 있다.
커스텀 Executor를 이용한 CompletableFuture
다섯 개의 상점을 검색할 때는 1022ms, 아홉 개의 상점을 검색할 때는 1022ms가 소요된다.
400개의 상점까지 이 같은 성능을 유지할 수 있다.
결국 애플리케이션 특성에 맞는 Executor를 만들어 CompletabelFuture를 활용하는 것이 바람직하다.
비동기 동작을 많이 사용하는 상황에서는 이 기법이 제일 효과적이다.
12 Jul 2024
이펙티브 자바라는 책의 내용 중에 상수라면 public static final 필드로 공개해도 괜찮고,
클래스에서 public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공해서는 안 된다고 한다.
나는 public static final 배열 필드를 비슷하게 Security에서 ALLOW URL을 관리를 위해 사용했었다.
filter에 허용이 필요한 url이나 테스트 용도로 작성하기 편했기 때문이다.
왜 public static final 배열 필드를 반환하는 접근자 메서드를 제공하면 안될까?
간단한 코드를 만들어보자. 아래와 같은 public static final 배열이 있다고 가정해보자.
위 코드는 변경이 가능한 보안 허점이 있다.
public static final로 선언된 배열 자체는 변경이 불가능하지만 배열의 요소는 변경이 될 수 있기 때문이다.
즉, 배열의 참조는 불변이지만 배열의 내용은 변경이 가능한 것이다.
먼저 보안 허점을 생각하지 못하게 한 final을 확인해보자. Test.ARR 에 새로운 String[] 배열을 넣으면 에러가 발생한다.
하지만 배열의 참조를 변경하지 말고 배열의 요소를 변경해보면, 에러가 발생하지 않고 변경이 된다!
그렇다면 어떻게 이러한 변경 가능성을 어떻게 막아야 할까?
책에서는 두 가지 방법을 제시한다.
- 앞 코드의 public 배열을 private로 만들고 public 불변 리스트를 추가한다.
- 배열을 private로 만들고 그 복사본을 반환하는 public 메서드를 추가한다 (방어적 복사)
불변 리스트는 unmodifiableList로 만든다.