2025-10-12

오늘 배운 것

자바에서 JVM의 구성 요소를 설명해주세요

JVM은 Java Virtual Machine으로, 자바 프로그램이 플랫폼에 상관없이 동작할 수 있도록 해주는 가상 실행 환경입니다. 이러한 JVM은 Class Loader, Runtime Data Area, Execution Engine, Native Interfacee로 크게 4가지로 구성되어 있습니다.

Class Loader는 .class 파일을 읽어 메모리에 올리고, 필요한 클래스를 동적으로 로드하는 역할을 합니다. 이를 통해 자바는 프로그램 실행 중에도 클래스를 유연하게 불러올 수 있습니다.

Runtime Data Area는 프로그램이 실행되는 동안 데이터를 저장하는 영역으로, 클래스 메타 정보, static 변수, 상수를 저장하는 메서드 영역, 객체 인스턴스를 저장하는 힙 영역, 메서드 호출 시 지역 변수나 매개변수를 저장하는 스택, PC 레지스터, 네이티브 메서드 스택으로 구성됩니다.

Excution Engine은 클래스 로더가 로드한 바이트 코드를 실제 기계어로 변환해 실행합니다. 이 안에는 인터프리터, JIT 컴파일러, Garbage Collector가 포함되어 있습니다. JIT 컴파일러는 자주 실행되는 코드를 미리 컴파일해 성능을 높이고, GC는 더이상 사용되지 않는 객체를 자동으로 정리해 메모리를 효율적으로 관리합니다.

마지막으로 Native Interface는 자바 코드가 C/C++로 작성된 네이티브 라이브러리를 호출할 수 있게 해주는 다리 역할을 합니다.

이러한 구조 덕분에 자바는 Write Once, Run Anywhere라는 특징을 가질 수 있습니다.

Garbage Collection 과정과 종류에 대해서 설명해주세요

GC는 JVM에서 더이상 사용되지 않는 객체를 자동으로 제거하여 메모리를 효율적으로 관리하는 기능입니다. 개발자가 명시적으로 메모리를 해제하지 않아도 되기 때문에, 자바의 큰 장점 중 하나입니다.

GC는 기본적으로 힙 영역의 객체를 대상으로 동작하며 객체가 도달 불가능 상태일 때 해당 객체를 가비지로 판단해 메모리를 회수합니다

GC의 기본 과정은 Mark, Sweep, Compact 단계로 구성됩니다. Mark 단계에서는 스택이나 정적 변수 등과 같은 루트 객체에서 참조 가능한 객체들을 탐색하고 살아 있는 객체로 표시합니다. Sweep 단계에서는 표시되지 않은 객체들을 제거하여 메모리를 비웁니다. Compact 단계에서는 객체 제거로 인해 비어있는 메모리 공간을 한쪽으로 몰아 단편화를 방지합니다. 이 과정은 Stop the World가 발생해 잠시 애플리케이션이 멈추기도 합니다.

GC는 메모리 구조에 따라 크게 Young Generation과 Old Generation으로 나뉩니다. Young Generation은 새로 생성된 객체가 저장되는 영역으로, 대부분의 객체는 여기서 생성되고 곧바로 사라집니다. Minor GC가 발생하며, 살아남은 객체는 Old 영역으로 승격됩니다. Old Generation은 오래 살아남은 객체가 저장되는 영역으로 Major GC가 발생하며, Young 영역과 Old 영역 전체를 대상으로 정리하기 때문에 상대적으로 시간이 오래 걸립니다.

자바에서 사용되는 주요 GC 알고리즘 종류로는 Serial GC, Parallel GC, CMS(Concurrent Mark Sweep), G1 GC(Garbage First) 등이 있습니다. G1 GC가 최근 JDK 기본 GC 알고리즘으로 힙으르 여러 구역으로 나누어 필요한 부분만 수집하여 응답시간이 예측 가능하다는 장점이 있습니다.

==.equals()의 차이는?

== 연산자는 객체의 주소값을 비교하고 .equals() 메서드는 객체가 가진 실제 값을 비교합니다.

멀티스레드 환경에서 동기화를 어떻게 보장하나요?

멀티스레드 환경에서는 여러 스레드가 동시에 같은 자원에 접근할 수 있기 때문에 데이터의 일관성이 깨질 수 있습니다. 이 문제를 해결하기 위해 동기화를 사용하는 데 자바에서는 가장 기본적인 방법으로 synchronized 키워드를 사용합니다.

이 키워드를 메서드나 블록에 적용하면 한 번에 하나의 스레드만 해당 코드 영역을 실행할 수 있어 경쟁 상태를 방지합니다.

또한 필요에따라 ReentrantLock 같은 Lock 객체로 세밀한 제어를 하거나, AtomicInteger 같은 원자적 클래스를 활용해 경량화된 동기화를 구현할 수도 있습니다.

synchronized와 ReentrantLock의 차이는 무엇인가요?

synchronized는 JVM 수준에서 제공되는 기본 동기화 방식으로 코드가 간단하지만, 명시적으로 Lock을 해제할 수 없습니다. 반면 ReentrantLock은 프로그래머가 직접 Lock을 걸고 해제할 수 있어 타임아웃 설정, 조건 변수 사용 같은 세밀한 동기화 제어가 가능합니다.

Atomic 클래스는 어떻게 동작하나요?

AtomicInteger 같은 Atomic 클래스는 CAS(Compare And Swap) 연산을 이용해 스레드 간 락을 걸지 않고도 안전하게 값을 변경할 수 있습니다. 즉, 락보다 가벼운 연산으로 동기화를 구현해 성능을 높이는 방식입니다.

동기화를 과도하게 사용하면 어떤 문제가 발생하나요?

불필요한 동기화는 성능 저하를 일으킵니다. 모든 스레드가 락을 기다리면서 병목 현상이 생기고, 잘못된 사용 시 교착 상태(Deadlock) 가 발생할 수도 있습니다. 그래서 공유 자원 접근이 꼭 필요한 구간에만 최소한으로 사용하는 것이 중요합니다.

CAS(Compare And Swap) 연산이 무엇인가요?

CAS(Compare And Swap)는 동기화 없이 여러 스레드가 동시에 데이터를 안전하게 수정할 수 있도록 하는 원자적 연산입니다.

CPU가 현재 값과 기대 값(expected value) 을 비교하고, 일치하면 새로운 값(new value) 으로 교체합니다. 일치하지 않으면 다른 스레드가 이미 값을 바꾼 것이므로 다시 시도합니다.

CAS는 락을 사용하지 않기 때문에 병목이 적고 성능이 좋습니다. 특히 경합이 적은 환경에서는 synchronized보다 훨씬 빠르게 동작합니다. 그래서 Java의 java.util.concurrent 패키지에서 많이 사용됩니다.

CAS는 값이 바뀔 때까지 반복해서 재시도(Spin) 해야 하므로, 경합이 심한 상황에서는 CPU 사용률이 높아질 수 있습니다. 또한 “ABA 문제”라는 특수한 상황이 발생할 수 있는데, 값이 A → B → A로 바뀌면 변화가 없다고 잘못 판단하는 경우입니다.

이 현상을 해결하기 위해 AtomicStampedReference 같은 클래스에서 버전 번호(스탬프) 를 함께 관리해 해결합니다. 값뿐 아니라 버전도 비교하기 때문에, 값이 다시 A로 돌아와도 버전이 다르면 변경으로 인식할 수 있습니다.

자바에서 메모리 누수는 어떤 경우에 발생할 수 있나요?

프로그램이 더 이상 사용하지 않는데도 객체의 참조가 남아 있는 경우에는 메모리 누수가 발생할 수 있습니다. 예를 들어 Collection에 객체를 넣고 제거하지 않거나 이벤트 리스너 또는 콜백을 제거하지 않거나 정적 변수에 객체를 저장하는 예시를 들 수 있습니다.

메모리 누수 예방 습관

  • Collection에 저장한 객체는 사용 후 반드시 제거
  • Listener, Stream, File, DB Connection 등은 명시적으로 닫기
  • Static 필드에는 불필요한 참조 보관 금지
  • AutoCloseable 자원은 try-with-resources로 관리

스프링 IoC와 DI 개념을 설명해주세요

스프링의 IoC는 객체의 생성과 관리 제어권을 개발자가 아닌 스프링 컨테이너에게 위임하는 개념입니다. 즉, 객체를 직접 생성하거나 의존 관계를 연결하지 않고, 스프링이 대신 관리해줍니다.

이러한 IoC의 개념을 구현한 것이 DI입니다. DI는 한 객체가 사용할 다른 객체를 외부에서 주입받는 방식으로 주로 생성자 주입, 세터 주입, 필드 주입 형태로 이루어집니다.

이를 통해 객체 간 결합도를 낮추고, 테스트나 유지보수가 쉬운 구조를 만들 수 있습니다.

왜 IoC와 DI가 필요할까요?

객체 간 의존 관계를 스프링이 관리하므로 개발자가 직접 new로 생성하지 않아도 되고, 코드 변경 없이 다른 구현체로 쉽게 교체할 수 있습니다. 따라서 유연성과 확장성이 높아지고 테스트 코드 작성도 훨씬 쉬워집니다.

의존성 주입 방법에는 어떤 것들이 있나요?

가장 권장되는 방식인 생성자 주입은 final 필드를 활용해 불변성으르 보장합니다. 세터 주입은 선택적인 의존성에 적합합니다. 필드 주입은 코드가 간결하지만, 테스트가 어려워 현재는 권장되지 않습니다.

IoC 컨테이너가 Bean을 관리한다는 건 무슨 뜻인가요?

스프링은 애플리케이션 실행 시 Bean 객체를 생성하고 그 생성, 초기화 ,사용, 소멸의 생명주기를 컨테이너가 직접 관리하여 컨테이너가 객체를 자동으로 주입하고 관리하는 것을 의미합니다.

AOP를 왜 사용하나요?

AOP는 관점 지향 프로그래밍으로 프로그램 전반에 걸쳐 공통적으로 필요한 기능을 핵심 로직과 분리해서 모듈화하기 위한 개념입니다.

예를 들어, 로깅, 트랜잭션 처리, 보안 검증, 실행 시간 측정 같은 기능은 여러 클래스에서 반복적으로 등장하지만, AOP를 사용하면 이들을 한 곳에 모아 관리할 수 있습니다.

스프링에서는 @Aspect와 @Around, @Before, @After 등의 어노테이션을 이용해 특정 메서드 실행 전후에 공통 기능을 삽입할 수 있습니다.

AOP를 사용하면 어떤 장점이 있나요?

핵심 로직과 공통 기능을 분리함으로써 관심사의 분리(Separation of Concerns) 가 이루어집니다. 즉, 코드 중복이 줄고, 유지보수가 쉬워지며, 공통 기능을 한 곳에서 수정하면 모든 서비스에 일괄 적용할 수 있습니다.

AOP는 내부적으로 어떻게 동작하나요?

스프링은 프록시 객체를 생성해 메서드 호출 전후에 부가 기능을 실행합니다. 디폴트로는 JDK Dynamic Proxy 또는 CGLIB Proxy를 사용하며, @Aspect로 정의된 코드가 실제 메서드 실행 전에 가로채는 방식으로 동작합니다.

트랜잭션 처리도 AOP로 구현된다고 들었는데, 맞나요?

맞습니다. 스프링의 @Transactional 어노테이션도 내부적으로 AOP를 활용합니다. 트랜잭션 시작과 커밋, 롤백 로직을 메서드 호출 전후에 프록시로 적용해 개발자가 비즈니스 로직에만 집중할 수 있게 해줍니다.

@Transactional 동작 원리를 아시나요?

트랜잭셔널은 스프링의 AOP 기반으로 동작합니다. 스프링은 트랜잭셔널 애너테이션이 붙은 메서드나 클래스에 대해 프록시 객체를 생성하고, 메서드 실행 전 후에 트랜잭션을 시작하고 커밋 또는 롤백하는 로직을 자동으로 삽입합니다.

프록시 기반이라고 했는데, 어떤 프록시가 사용되나요?

스프링은 기본적으로 인터페이스가 있으면 JDK Dynamic Proxy, 클래스만 있으면 CGLIB Proxy를 사용하여 메서드 호출 전후에 부가 로직을 실행합니다.

롤백은 언제, 어떤 예외에서 발생하나요?

기본적으로 Runtime Exception 또는 에러가 발생하면 롤백합니다. Checked Exception은 기본 설정으로는 롤백하지 않지만, rollbackFor 속성을 지정하면 원하는 예외에도 롤백되도록 설정할 수 있습니다.

같은 클래스 내부에서 @Transactional 메서드를 호출하면 적용이 안 되던데, 왜 그런가요?

AOP는 프록시 객체를 통해서만 동작하기 때문에 같은 클래스 내부에서 자기 자신의 메서드를 직접 호출하면 프록시를 거치지 않습니다. 따라서 트랜잭션이 적용되지 않습니다.

이를 해결하기 위해서는 메서드를 다른 빈으로 분리하거나 AopContext.currentProxy()를 활용해 자기 자신 프록시를 통해 호출해야합니다.

프록시 객체란 무엇인가요?

프록시 객체는 대상 객체를 감싸서 대신 동작하는 객체입니다. 즉, 클라이언트는 진짜 객체를 직접 호출하지 않고 프록시를 통해 호출하게 되며, 프록시는 호출 전후에 부가 기능을 수행한 뒤 실제 객체를 실행합니다.

RESTful API 설계 시 고려할 점은?

자원 중심의 일관된 구조와 표준 HTTP 규약 준수를 가장 중요하게 고려해야 합니다.

  1. URI는 자원을 명확히 표현해야 합니다.
    • 동사는 사용하지 않고 명사로 작성
    • 계층 구조로 표현
  2. HTTP 메서드는 행위를 나타냅니다.
    • GET : 조회
    • POST : 생성
    • PUT : 전체 수정
    • PATCH : 일부 수정
    • DELETE : 삭제
  3. HTTP 상태 코드를 일관되게 사용해야 합니다.
    • 200 : OK
    • 201 : Created
    • 400 : Bad Request
    • 404 : Not Found
    • 500 : Internal Server Error
  4. 예외 처리와 응답 메시지 구조를 표준화합니다.
    • 응답 본문에는 code, message, data 형태로 통일
    • 클라이언트가 에러 원인을 명확히 파악할 수 있게 작성
  5. 보안과 인증을 고려합니다.
    • JWT, OAuth2 기반 인증/인가 적용
    • 민감 정보는 HTTPS로 암호화
  6. 확장성과 버전 관리
    • /api/v1/.. 형태로 버전 명시
    • 하위 호환성을 유지하면서 점진적으로 개선

Spring Boot의 장점과 자동 설정(Auto Configuration) 원리는 무엇인가요?

스프링 부트는 기존 스프링의 복잡한 설정을 줄이고, 개발자가 비즈니스 로직에 집중할 수 있도록 자동 설정과 내장 서버를 제공하는 프레임워크입니다.

  1. 자동 설정(Auto Configuration) 필요한 라이브러리를 추가하면 스프링이 자동으로 관련 Bean을 등록합니다. 예를 들어, spring-boot-start-web을 추가하면 DispatcherServlet, Tomcat, ObjectMapper 등을 자동으로 설정합니다.
  2. 내장 서버 제공 Tomcat, Jetty, Undertow가 내장되어 있어 WAR 배포 없이 바로 실행 가능
  3. 의존성 관리 편의성 spring-boot-starter로 관련 라이브러리를 한번에 관리하고 버전 호환성이 자동으로 맞춰짐
  4. 프로파일 관리 용이 application.yml 또는 .properties에서 환경별 설정 분리
  5. 모니터링 및 헬스체크 spring-boot-actuator를 통해 애플리케이션 상태, 메트릭 확인 가능

자동 설정 원리는 스프링 부트가 실행시 @SpringBootApplication 안에 포함된 @EnableAutoconfiguration 어노테이션을 통해 자동 설정을 수행합니다. 이 어노테이션으느 내부적으로 spring.factories 파일에 등록된 설정 클래스들을 읽어옵니다.

클래스 패스에 존재하는 라이브러리를 기준으로 해당 설정을 조건부로 적용하여 필요한 Bean만 자동으로 등록하고, 이미 등록된 Bean이 있으면 중복 설정을 하지 않습니다.

@SpringBootApplication 안에는 어떤 어노테이션이 포함되어 있나요?

-@SpringBootApplication은 세 가지 어노테이션을 합쳐놓은 것입니다.

  • @SpringBootConfiguration → 스프링 설정 클래스임을 의미
  • @EnableAutoConfiguration → 자동 설정 활성화
  • @ComponentScan → 지정한 패키지 하위에서 Bean 자동 등록

Bean의 생명주기를 간단히 설명해주세요

스프링 Bean은 컨테이너가 생성부터 소멸까지 관리합니다.

  1. 객체 생성 스프링이 @Component, @Bean 등으로 등록된 클래스를 인스턴스화(빈)합니다.
  2. 의존성 주입 생성된 Bean에 필요한 다른 Bean을 주입합니다.
  3. 초기화 Bean이 주입을 모두 받은 후, @PostConstruct 메서드나 IntializingBean의 afterProptiesSet()이 호출됩니다. 그리고 DB 연결이나 리소스 로드 등의 초기 설정 작업을 수행합니다.
  4. 사용 컨테이너가 완전히 초기화 된 후, 애플리케이션에서 Bean이 실제로 사용됩니다.
  5. 소멸 애플리케이션 종료 시, @PreDestroy 메서드나 DisposableBean의 destroy()가 호출되어 자원을 해제합니다.

이 모든 과정으르 스프링 컨테이너가 자동으로 관리합니다.

Bean의 생명주기 콜백을 왜 사용하는 건가요?

DB 커넥션, 파일 핸들, 소켓 등의 외부 자원을 초기화하거나 종료할 때 생명주기 콜백을 사용하면 Bean이 완전히 준비된 후 안전하게 초기화하고, 컨테이너 종료 시점에 자원을 확실히 해제할 수 있습니다.

JPA 장단점은 무엇인가요?

JPA(Java Persistence API)는 자바 객체와 데이터베이스 테이블을 매핑해주는 ORM(Object Relational Mapping) 기술 표준입니다.

즉, SQL을 직접 작성하지 않아도 객체 중심으로 데이터를 다룰 수 있도록 도와줍니다.

장점

  1. 생산성 향상 save(), findById() 같은 메서드만으로 CRUD가 가능해 SQL 작성량이 줄어들어 개발자가 비즈니스 로직에 집중할 수 있습니다.
  2. 유지보수성 및 재사용성 테이블 구조 변경 시에도 객체 필드만 수정하면 되므로, SQL 쿼리 전체를 바꿀 필요가 없습니다.
  3. 객체 지향적인 데이터 접근 객체 간 연관 관계르르 @OneToMany, @ManyToOne 같은 애너테이션으로 그대로 표현할 수 있어 객체 모델링과 DB 설계를 일관성 있게 유지할 수 있습니다.
  4. 캐싱과 변경 감지(Dirty Checking) 1차 캐시, 쓰기 지연 등으로 성능을 최적화하고, 엔티티의 변경사항을 자동으로 감지해 update 쿼리를 생성합니다. 단점
  5. 복잡한 쿼리 제어의 어려움 단순 CRUD는 편리하지만, 복잡한 통계/집계 쿼리는 JPQL로 작성하기 어렵습니다. 성능 이슈 때문에 Native SQL을 함께 사용해야 하는 경우도 많습니다.
  6. 성능 튜닝 난이도 지연 로딩과 즉시 로딩 설정을 잘못하면 N+1 문제가 발생할 수 있습니다.
  7. 학습 곡선이 높음 영속성 컨텍스트, 플러시, 캐시 동작 원리 들을 내부 구조를 이해해야 예기치 못한 동작을 피할 수 있습니다.

N+1 문제는 무엇인가요?

연관된 엔티티를 조회할 때, 한번의 쿼리로 부모 엔티티를 가져오고 각 자식 엔티티를 개별쿼리로 조회하면서 총 N+1번의 SQL이 발생하는 현상입니다. fetch join이나 @EntityGraph를 활용해 해결할 수 있습니다.

JPA를 사용할 때 성능을 개선하기 위한 방법에는 어떤 게 있나요?

  • fetch join으로 N+1 문제 해결
  • 캐시(1차 캐시, 2차 캐시) 활용
  • JPQL 대신 QueryDSL로 타입 안정성 확보
  • 배치 크긔 조정으로 Lazy Loading 최적화

JPA 영속성 컨텍스트는 어떤 역할을 하나요?

영속성 컨텍스트는 엔티티를 1차 캐시로 관리하며, 같은 트랜잭션 내에서 동일한 엔티티는 항상 동일한 객체로 유지됩니다. 또한 Dirty Checking으르 통해 변경된 내용을 자동으로 DB에 반영합니다.

자바 17버전 특징에 대해서 알고 있는게 있나요? 왜 17버전을 사용했나요?

Java 17은 LTS 버전으로 안정성과 성능이 강화된 환경을 제공합니다. Record, Sealed Class, Switch 패턴 매칭 등 문법 개선으로 코드가 간결해졌고, G1·ZGC 개선으로 대규모 서비스에서도 안정적인 성능을 유지할 수 있습니다.

Java 17은 LTS(Long Term Support) 버전으로, 장기적으로 안정성과 호환성이 보장되기 때문에 선택했습니다. 실무 환경에서는 장기 지원 버전을 사용하는 것이 유지보수에 유리하고, GC 성능 개선과 Record, Sealed Class 같은 최신 문법도 활용할 수 있습니다. 특히, Spring Boot 3 이상은 JDK 17을 기본 지원하기 때문에 최신 스프링 생태계와의 호환성 측면에서도 가장 적합한 버전이라고 판단했습니다

results matching ""

    No results matching ""