[면접] 기술 면접 - Java (1)
- Call by reference란 무엇이고 보통 어떻게 쓰이나요?
" Java에서 "call by reference"는 함수를 호출할 때 객체의 메모리 주소를 전달하는 방식을 의미합니다. 이를 사용하면 함수 내에서 객체의 값을 변경했을 때, 해당 변경사항이 함수 밖에 있는 원본 객체에도 적용됩니다. 이 방식은 함수를 통해 객체의 상태를 직접 수정해야 할 경우에 주로 사용됩니다. "call by reference"를 활용하면 함수 외부의 변수나 객체 상태에 영향을 줄 수 있으므로, 객체 지향 프로그래밍에서는 이 개념이 매우 중요합니다.
따라서, 스프링(Spring)은 Java에서 메소드 호출 시 “call by value" 방식을 따릅니다. 이는 메소드에 변수를 전달할 때 해당 변수의 값이 복사되어 전달되며, 메소드 내에서 변수의 값을 변경해도 호출자의 변수는 변경되지 않습니다. "
꼬리질문 > Java에서 'call by reference'와 'call by value'의 차이점은 무엇인가요?
"Java에서 'call by value' 방식은 함수가 매개변수로 원시 타입(primitive type)의 실제 값을 받아, 함수 내에서 이 값을 변경해도 원본 값에는 영향을 주지 않습니다. 반면, 'call by reference' 개념에 대해 자주 언급되지만, Java는 엄밀히 말하면 'call by value'만을 지원합니다. 이는 객체의 참조(주소)를 전달할 때, 참조 자체의 값이 복사되어 함수에 전달되기 때문입니다. 함수 내에서 이 참조를 통해 객체를 수정하면, 원본 객체에도 변경이 반영됩니다. 따라서, 객체의 상태 변경이 가능하지만, 전달된 참조 자체를 변경해도 원본 참조에는 영향을 미치지 않습니다. 이러한 방식은 객체를 효율적으로 관리하고, 메모리 사용을 최적화하는 데 도움이 됩니다."
꼬리질문 > Java에서 'call by refernce'와 유사하게 작동하는 인자가 있을까요?
" Java에서 'call by reference'와 유사하게 작동하는 것은 클래스의 객체 참조입니다. 객체를 메서드에 전달할 때, 그 객체의 참조값이 'call by value' 방식으로 전달됩니다. 이 참조를 통해 객체의 상태를 변경할 수 있기 때문에, 외관상 'call by reference'처럼 보일 수 있습니다. 그러나 실제로는 참조값의 복사본이 전달되므로, 원본 객체는 변경 가능하지만, 참조 자체를 변경하는 것은 외부에 영향을 주지 않습니다."
- Override 와 Overload 를 설명해주실 수 있을까요?
" 네, Override와 Overload는 프로그래밍에서 중요한 두 가지 방법으로, 다양한 상황에서 코드를 유연하게 작동하게 해주는 역할을 합니다.
Override는, 간단히 말해, 자식 클래스가 부모 클래스에 있는 메서드를 새로운 방식으로 다시 작성하는 것을 말합니다. 이렇게 하면 같은 이름의 메서드라도 상황에 맞게 다르게 동작하게 할 수 있어, 같은 기능이지만 상황에 따라 다르게 반응해야 할 때 유용합니다.
Overload는, 같은 이름을 가진 메서드를 여러 개 만들되, 각각 다른 매개변수를 받도록 하는 것입니다. 이를 통해 하나의 메서드 이름으로 다양한 입력 형태를 처리할 수 있게 되어, 같은 종류의 작업을 여러 방식으로 쉽게 처리할 수 있습니다. "
꼬리질문 > Override와 Overload를 사용할 때 주의해야 할 점은 무엇인가요?
"Override 시 상위 클래스의 메서드 시그니처와 일치해야 하며, 접근 제한을 강화할 수 없습니다. `@Override` 어노테이션을 사용해 정확성을 검증하는 것이 좋습니다. Overload는 매개변수의 타입, 개수, 순서가 달라야 하며, 반환 타입만으로는 구분되지 않습니다. 오버로딩 시 명확성을 유지하고, 가독성이 떨어지지 않도록 주의해야 합니다. Override와 Overload 모두 사용할 때는 코드의 명확성과 가독성을 중시해야 합니다."
- JPA는 언제 필요하고 언제 필요하지 않은지 설명해주실 수 있을까요?
" 네, JPA(Java Persistence API)는 특히 데이터베이스와 상호작용하는 복잡한 자바 애플리케이션을 개발할 때 큰 장점을 제공합니다. 예를 들어, 여러 데이터 모델 간의 복잡한 관계를 관리해야 할 때, JPA를 사용하면 이를 효율적으로 매핑하고 간결한 코드로 처리할 수 있습니다.
JPA는 데이터베이스 작업을 자바 객체로 추상화하여, SQL 쿼리를 직접 작성하는 대신 객체 지향 방식으로 데이터를 처리할 수 있게 해주므로, 개발자로서는 보다 직관적이고 개발 친화적인 환경에서 작업할 수 있습니다. 이를 통해 애플리케이션의 유지보수성과 개발 효율성이 크게 향상됩니다.
하지만, JPA가 항상 모든 상황에 최적의 선택은 아닙니다. 예를 들어, 프로젝트의 요구 사항이 매우 단순하고 단순한 CRUD 연산만 필요한 경우에는, JPA보다는 JDBC나 MyBatis 같은 경량 프레임워크가 더 적합할 수 있습니다. 또한, 최적의 성능을 요구하는 시스템이나 매우 복잡하고 특수한 SQL 쿼리가 필요한 상황에서는 JPA의 사용이 제한적일 수 있습니다. 이런 경우에는 직접 SQL을 작성하거나 다른 접근 방식을 고려하는 것이 더 효과적일 수 있습니다.
결론적으로, JPA는 개발자로 하여금 객체 지향적인 접근을 통해 데이터베이스 작업을 효과적으로 처리할 수 있게 하지만, 프로젝트의 특성과 요구 사항을 고려하여 적절한 기술을 선택해야 합니다. "
꼬리질문 > JPA 사용 시 고려해야 할 성능 관련 문제는 무엇이 있나요?
" JPA 사용 시 성능 문제로 N+1 쿼리 문제, 캐싱 전략 오류, 자동 생성 SQL의 비효율성을 고려해야 합니다. 우선, N+1 쿼리 문제는 연관된 엔티티를 로딩할 때, 한 번의 쿼리로 연관된 엔티티들을 모두 가져오지 않고, 각 엔티티를 가져올 때마다 추가 쿼리가 실행되어 성능 저하를 일으키는 문제입니다. 이를 방지하기 위해 페치 조인(fetch join)을 사용하여 관련 엔티티를 미리 로딩할 수 있습니다. 다음으로, 캐싱 전략을 잘못 사용하면 예상치 못한 성능 문제가 발생할 수 있습니다. JPA는 1차 캐시(퍼시스턴스 컨텍스트 내 캐시)와 2차 캐시(애플리케이션 전반에 걸친 캐시)를 제공합니다. 적절한 캐싱 전략을 사용하지 않으면, 메모리 사용량 증가나 불필요한 데이터 캐싱으로 인해 성능이 저하될 수 있습니다. 마지막으로 JPA의 자동 생성되는 SQL 쿼리가 항상 최적의 성능을 제공하지는 않습니다. JPA는 개발자의 편의를 위해 복잡한 쿼리를 자동으로 생성해주지만, 경우에 따라 수동으로 최적화된 쿼리를 작성하는 것이 더 나은 성능을 제공할 수 있습니다. JPA를 사용할 때는 이러한 성능 관련 문제들을 사전에 고려하고, 적절한 전략과 최적화를 통해 해결해야 합니다."
- JPA의 더티 체킹이란 무엇인가요?
" JPA에서 더티 체킹(dirty checking)이란 JPA가 자동으로 엔티티의 변경사항을 감지하고, 필요한 경우 데이터베이스에 이를 반영하는 과정을 의미합니다. 여기서 '더티(Dirty)'란 "변경된 부분"을 의미하여 즉, 더티 체킹은 "변경 감지"라고도 할 수 있습니다.
영속성 컨텍스트(Persistence Context)는 엔티티를 처음 불러올 때 생성되며, 이후 엔티티의 변경을 감지하는 역할을 합니다. 영속성 컨텍스트는 엔티티의 생명주기를 관리하고, 특정 엔티티가 변경되었는지 계속 주시합니다. 하지만, 준영속 또는 비영속 상태의 엔티티는 이 변경 감지 대상에서 제외됩니다.
트랜잭션이 진행되는 동안, 영속성 컨텍스트는 엔티티의 변경사항을 추적만 하고 실제 데이터베이스에는 반영하지 않습니다. 그러다 트랜잭션이 성공적으로 마무리되어 커밋되는 순간, 변경된 엔티티의 상태가 데이터베이스에 반영됩니다. 이 과정 덕분에, 개발자는 데이터의 일관성을 유지하면서도, 변경사항을 쉽게 관리할 수 있습니다. "
꼬리질문 > JPA 사용 시 고려해야 할 성능 관련 문제는 무엇이 있나요?
"JPA의 더티 체킹 기능은 개발 편의성을 크게 향상시키지만, 몇 가지 주의해야 할 문제가 있습니다. 첫째, 성능 문제가 발생할 수 있습니다. JPA는 트랜잭션이 종료될 때 변경된 모든 엔티티를 검사하여 데이터베이스에 반영합니다. 이 과정에서 변경되지 않은 많은 필드까지 검사하게 되어, 큰 규모의 엔티티나 복잡한 객체 구조를 가진 애플리케이션에서 성능 저하를 초래할 수 있습니다.
둘째, 예상치 못한 데이터베이스 업데이트가 발생할 수 있습니다. 개발자가 의도하지 않게 엔티티의 상태를 변경한 경우, 그 변경 사항이 자동으로 데이터베이스에 반영될 수 있습니다. 이는 데이터 일관성 문제를 일으킬 수 있으며, 디버깅을 어렵게 만들 수 있습니다.
셋째, 트랜잭션 관리가 중요해집니다. 더티 체킹은 트랜잭션의 범위 내에서 이루어지므로, 트랜잭션을 정확히 관리하지 않으면 예상치 못한 시점에 데이터베이스 업데이트가 발생할 수 있습니다. 이는 특히 분산 시스템이나 복잡한 트랜잭션을 다루는 애플리케이션에서 주의를 요합니다.
따라서, 더티 체킹 기능을 효율적으로 사용하기 위해서는 엔티티의 변경 관리, 트랜잭션의 명확한 정의와 관리, 성능 최적화 전략에 대한 깊은 이해가 필요합니다."
꼬리질문 > JPA에서 엔티티의 생명주기 상태 중 '비영속(detached)'과 '준영속(detached)' 상태일 때 업데이트 처리는 어떻게 하나요?
"JPA에서 엔티티가 비영속(new) 상태일 때는 아직 JPA 컨텍스트와 연결되지 않았기 때문에, JPA는 이 엔티티의 변경사항을 추적하거나 데이터베이스에 반영하지 않습니다. 엔티티를 데이터베이스에 저장하고 싶다면, 엔티티 매니저의 `persist()` 메소드를 사용해 해당 엔티티를 영속 상태로 만들어야 합니다.
준영속(detached) 상태의 엔티티는 이전에 영속 상태였으나 현재는 JPA 세션에서 분리되어 변경 감지(dirt checking) 대상이 아닙니다. 준영속 상태의 엔티티에 대한 변경사항을 데이터베이스에 반영하고자 할 때는, `merge()` 메소드를 사용합니다. `merge()`는 준영속 상태의 인스턴스를 받아서 그 변경사항을 새로운 영속 상태의 인스턴스에 복사한 후, 그 인스턴스를 반환합니다. 이 과정에서 변경된 사항이 데이터베이스와 동기화됩니다.
결국, 비영속 상태에서는 `persist()`를 사용해 영속 상태로 만들어 JPA가 관리하도록 하고, 준영속 상태에서는 `merge()`를 사용해 변경사항을 새 영속 인스턴스에 복사하여 데이터베이스에 반영하는 방식으로 처리합니다."
꼬리질문 > @Transactional 메서드에서 Entity를 인자로 받았을 때 더티체킹이 작동하나요?
"`@Transactional` 메서드에서 엔티티를 인자로 받았을 때, 더티 체킹은 엔티티가 영속 상태일 경우에만 작동합니다. 영속 상태 엔티티의 변경사항은 트랜잭션이 끝날 때 자동으로 데이터베이스에 반영됩니다. 반면, 준영속 상태 엔티티의 변경사항은 `merge()`를 사용해 영속 상태로 전환한 후에만 반영됩니다."
- JVM 이란 무엇이고 왜 필요한지 설명해주실 수 있을까요?
" 네, JVM은 Java Virtual Machine으로 자바 프로그램을 실행시켜주는 가상 머신입니다. 일반적인 프로그램은 OS에 종속적이게 개발이 되었지만, Java는 각 OS에 맞게 제공된 JVM이 띄워질 수 있기에 같은 소스코드로 다른 OS에서 실행 할 수 있게 됩니다. JVM이 주는 이점으로는 이것 이외에도 GC를 통해 프로그램의 메모리 관리를 해준다는 점도 있습니다. "
꼬리질문 > JVM이 다른 프로그래밍 언어의 실행 환경과 비교하여 가지는 독특한 장점은 무엇인가요?
" JVM의 가장 큰 장점은 플랫폼 독립성입니다. 한 번 작성된 자바 프로그램은 JVM 위에서 다양한 운영 체제에서 변경 없이 실행될 수 있습니다. 이는 "Write Once, Run Anywhere"(WORA) 원칙을 가능하게 합니다. 또한, JVM은 프로그램 실행 중에 가비지 컬렉션과 메모리 관리를 자동으로 처리하여, 메모리 누수와 같은 문제를 최소화합니다. JVM은 또한 동적으로 클래스를 로드하는 기능을 제공하여, 런타임에 필요한 클래스만 메모리에 로드함으로써 효율적인 자원 사용이 가능합니다. JVM의 이러한 기능들은 안정성과 효율성을 높이며, 다양한 환경에서의 어플리케이션 실행을 간소화합니다."
- Java가 컴파일되는 과정은 어떻게 되는지 설명해주실 수 있을까요?
" 먼저, 자바로 작성된 코드는 '.java' 확장자를 가진 소스 파일에 저장됩니다. 이 코드는 컴파일 과정을 거쳐 '.class' 확장자를 가진 바이트코드라는 형태의 파일로 변환됩니다. JVM을 통해 이 바이트코드를 실행하며 필요한 경우 'JIT(Just-In-Time) 컴파일러'라는 기술을 사용하여 바이트코드를 더 빠르게 실행할 수 있는 기계어로 변환합니다.
간단히 말해, 자바 코드는 컴파일을 통해 운영체제에 독립적인 바이트코드로 변환되고, JVM이 이를 실행하여 다양한 OS에서 자바 애플리케이션을 사용할 수 있게 만듭니다. JIT 컴파일러는 이 과정에서 프로그램의 성능을 최적화해 줍니다. "
꼬리질문 > JIT 컴파일러는 자바 프로그램의 실행 과정에서 어떻게 성능을 최적화하나요?
" JIT(Just-In-Time) 컴파일러는 JVM 내에서 바이트코드를 실행 시점에 기계어로 변환하는 역할을 합니다. 이 과정에서 JIT 컴파일러는 프로그램 실행 중에 자주 사용되는 코드 부분(핫 스팟)을 식별하고, 해당 부분을 직접 기계어로 컴파일하여 성능을 최적화합니다. 이렇게 컴파일된 코드는 직접 실행되므로, 인터프리터 방식보다 실행 속도가 훨씬 빠릅니다. JIT 컴파일러는 실행 시간 동안에도 최적화 작업을 계속 수행하여, 프로그램의 성능을 동적으로 개선할 수 있습니다. 이런 JIT 컴파일의 도입으로 자바 애플리케이션은 높은 성능을 유지하면서도 플랫폼 독립성을 보장받을 수 있습니다."