개발 공부/자바

자바 공부 일지 (면접 질문 위주)

baby-t 2026. 2. 16. 23:04

최근 열정적으로 자바와 스프링을 공부 중인데, 그냥 개인적으로 암기를 하다, 왜 그동안 블로그로 작성하다가 안하고 있지? 란 생각이 들어 다시 공부 내용들을 일지처럼 작성하겠습니다. 직관성을 위해 제미나이로 문장을 다듬었습니다.

 

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

여태 한 내용 (02.16)

1. 자바 컴파일 과정

프로젝트를 실행하면 빌드 단계에서 자바 컴파일러(javac)가 소스 파일(.java)을 JVM이 이해할 수 있는 바이트 코드(.class)로 변환한다. 이후, JVM의 클래스 로더(Class Loader)가 바이트 코드를 Runtime Data Area(메모리) 중 Method Area에 로드한다. 로딩된 바이트 코드는 실행 엔진(Execution Engine)에 의해 기계어(Binary Code)로 변환되어 실행된다. 실행 엔진에는 인터프리터(Interpreter)와 JIT(Just In Time) 컴파일러가 있다.

  • 평소: 인터프리터가 한 줄씩 해석하며 실행.
  • 최적화: 자주 반복되는 코드(Hot Code)는 JIT 컴파일러가 기계어로 한 번에 변환하여 캐싱(Caching)해두고 재사용하여 속도를 높인다.

2. String, StringBuilder, StringBuffer의 차이

  • String: 불변(Immutable) 객체. 문자열 연산 시 새로운 객체가 계속 생성된다.
  • StringBuilder: 가변(Mutable) 객체. 동기화(Synchronization)가 되어 있지 않아 멀티 쓰레드 환경에서는 위험하지만(Unsafe), 싱글 쓰레드에서는 속도가 가장 빠르다.
  • StringBuffer: 가변(Mutable) 객체. synchronized 키워드로 동기화 처리가 되어 있어 멀티 쓰레드 환경에서 안전(Thread-safe)하다.

2-0. String이 불변 객체인 이유와 원리 String은 리터럴("")과 new 연산자 두 가지 방식으로 생성된다. 리터럴로 생성 시 String Constant Pool(문자열 상수 풀)에 저장되어, 같은 값을 가진 변수들은 모두 같은 메모리 주소를 참조하게 된다. (메모리 절약)

  • 불변의 장점:
    1. 메모리 효율성: String Pool을 통해 중복 생성을 방지.
    2. 보안성: DB 연결 정보, 비밀번호 등이 변경되는 것을 방지.
    3. Thread-safe: 값이 바뀌지 않으므로 멀티 쓰레드에서 공유해도 안전함.

 

2-1 동기 처리, 비동기 처리란? safe mode, unsafe mode? (아직 안배움)

 

2-2 자바에서 싱글 쓰레드, 멀티 쓰레드? (아직 안배움)

 

3. 접근 제어자 (Access Modifier)

  • public: 모든 곳에서 접근 가능.
  • protected: 같은 패키지 + 다른 패키지의 상속받은 자식 클래스에서 접근 가능.
  • default: 같은 패키지 내에서만 접근 가능. (생략 시 기본값)
  • private: 해당 클래스 내부에서만 접근 가능.

4. OOP의 4가지 특징

  1. 캡슐화 (Encapsulation): 데이터(변수)와 기능(메서드)을 하나로 묶고, 접근 제어자를 통해 외부 접근을 제한하여 데이터를 보호(은닉화)한다.
  2. 상속 (Inheritance): 상위 클래스의 속성과 기능을 하위 클래스가 물려받아 재사용하거나 확장한다.
  3. 추상화 (Abstraction): 불필요한 세부 정보는 숨기고, 중요한 공통 특징(역할)만 뽑아내어 모델링한다. (예: 아이폰, 갤럭시는 '스마트폰'으로 추상화)
  4. 다형성 (Polymorphism): 하나의 객체나 메서드가 여러 형태를 가질 수 있는 성질.
    • 오버로딩: 같은 이름의 메서드를 매개변수를 다르게 하여 여러 개 정의.
    • 오버라이딩: 상위 클래스의 메서드를 하위 클래스에서 재정의.
    • 참조 변수의 다형성: Parent p = new Child(); 처럼 부모 타입으로 자식 객체를 참조하는 것.

5. OOP의 5대 원칙 (SOLID)

1) Single Responsibility Principle (SRP, 단일 책임 원칙) 한 객체는 하나의 책임만 가져야 한다는 것이다. 즉, 클래스를 변경해야 하는 이유는 단 하나여야 한다.

2) Open/Closed Principle (OCP, 개방-폐쇄 원칙) 기능 확장에는 열려 있고, 코드 수정에는 닫혀 있다는 것이다. 기존 코드를 뜯어고치지 않고도 새로운 기능을 추가할 수 있어야 한다. 이를 위해서는 **다형성(Polymorphism)**과 인터페이스를 적극 활용해야 한다.

3) Liskov Substitution Principle (LSP, 리스코프 치환 원칙) 자식 클래스는 언제나 부모 클래스를 대체할 수 있어야 한다는 것이다. 부모 클래스가 들어갈 자리에 자식 클래스를 넣어도 프로그램이 깨지지 않아야 한다.

실패 예시: '펭귄'이 '새' 클래스를 상속받았는데, 새 클래스에 fly() 메소드가 있다면? 펭귄은 날지 못하므로 fly()를 실행하면 에러가 나거나 억지로 예외 처리를 해야 한다. 이는 펭귄이 부모(새)를 완벽히 대체하지 못한 것이므로 LSP 위반이다. (상속 관계 재설계 필요)

4) Interface Segregation Principle (ISP, 인터페이스 분리 원칙) 범용적인 인터페이스 하나보다, 구체적인 여러 개의 인터페이스가 낫다. 클라이언트는 자신이 사용하지 않는 메소드에 의존하면 안 된다.

5) Dependency Inversion Principle (DIP, 의존 역전 원칙) **구체적인 클래스(구현체)**에 의존하지 말고, **추상화(인터페이스)**에 의존하라는 것이다.

예시 (스위치와 선풍기)

  • 나쁜 예 (DIP 위반): '스위치' 클래스가 '삼성 선풍기' 클래스를 직접 변수로 갖고 있다.
    • 문제: 삼성 선풍기를 'LG 선풍기'로 바꾸려면 스위치 클래스의 코드를 직접 뜯어고쳐야 한다. (스위치가 부품에 의존함)
  • 좋은 예 (DIP 준수): '스위치' 클래스는 **'전원 장치'**라는 인터페이스를 변수로 갖는다. 삼성 선풍기와 LG 선풍기는 모두 이 인터페이스를 구현한다.
    • 결과: 스위치는 연결된 게 뭔지 몰라도 "전원 켜기" 버튼만 누르면 된다. 부품을 바꿔도 스위치 코드는 변경할 필요가 없다.

6. JVM 메모리 구조 (Runtime Data Area)

크게 쓰레드 공유 영역과 고유 영역으로 나뉜다.

  • 공유 영역 (GC의 대상):
    • Method Area: 클래스 정보(설계도), static 변수, 상수 등이 저장됨.
    • Heap: new로 생성된 실제 객체(인스턴스)들이 저장됨.
  • 쓰레드별 고유 영역:
    • Stack: 메서드 호출 시 생성되는 프레임, 지역 변수, 매개변수가 저장됨. (Heap 영역의 객체 주소값을 참조)
    • PC Register: 현재 실행 중인 명령어의 주소를 가리킴.
    • Native Method Stack: 자바 외의 언어(C/C++ 등)로 작성된 코드를 위한 영역.

7. 클래스, 객체, 인스턴스

  • 클래스: 객체를 만들기 위한 설계도 (붕어빵 틀).
  • 객체 (Object): 구현할 대상, 개념적인 용어.
  • 인스턴스 (Instance): 설계도를 바탕으로 메모리(Heap)에 실제로 구현된 실체 (만들어진 붕어빵).

8. interface와 abstract class 차이, 언제 사용되는지

[차이점]

  • 인터페이스 (Interface): **구현(Implementation)**을 강제하는 것이 목적. 다중 상속 가능. 변수는 상수(static final)만 가능. (Can-Do 관계)
  • 추상 클래스 (Abstract Class): 기능을 물려받아 **확장(Extension)**하는 것이 목적. 단일 상속만 가능. 일반 변수와 일반 메소드도 가질 수 있음. (Is-A 관계)

[언제 사용하는가?]

1) 추상 클래스 (Abstract Class)

  • Is-A 관계일 때: "A는 B이다." (예: 고양이는 동물이다, 강아지는 동물이다)
  • 부모의 기능을 물려받되, 자식이 조금 고쳐서 쓰거나 확장해야 할 때.
  • 여러 클래스에서 공통으로 사용하는 **필드(멤버 변수)**나 일반 메소드가 많아서 중복을 줄이고 싶을 때.

2) 인터페이스 (Interface)

  • Can-Do 관계일 때: "~를 할 수 있는." (예: 수영할 수 있는, 날 수 있는)
  • 서로 관련 없는 클래스들에게 공통적인 동작을 강제하고 싶을 때.
  • 상속 계층 구조에 얽매이지 않고 자유롭게 기능을 부착하고 싶을 때. (예: Serializable, Comparable 등)

9. Checked Exception과 Unchecked Exception의 차이

자바에서는 프로그램 실행 중 발생하는 문제를 Throwable 클래스로 관리하며, 크게 ErrorException으로 나뉜다.

1) Error (에러) 시스템 레벨의 심각한 문제로, 개발자가 코드로 복구할 수 없는 수준이다.

  • 예: StackOverflowError (스택 메모리 넘침), OutOfMemoryError (힙 메모리 부족) 등.

2) Exception (예외) 개발자가 로직을 통해 예방하거나 처리(try-catch)할 수 있는 문제다. 예외는 다시 두 가지로 나뉜다.

  • Checked Exception (체크 예외)
    • 특징: 컴파일 시점에 확인된다. 반드시 예외 처리(try-catch 또는 throws)를 해야만 컴파일이 된다.
    • 발생: 주로 외부 자원(파일, 네트워크, DB)과 상호작용할 때 발생한다. (트랜잭션 롤백이 기본적으로 안 됨 - 설정 필요)
    • 예시: IOException, SQLException, ClassNotFoundException.
  • Unchecked Exception (언체크 예외)
    • 특징: 런타임(실행) 시점에 확인된다. (RuntimeException을 상속받음). 명시적인 예외 처리를 강제하지 않는다.
    • 발생: 주로 개발자의 로직 실수로 인해 발생한다. (트랜잭션 롤백이 기본적으로 됨)
    • 예시: NullPointerException, IndexOutOfBoundsException, ArithmeticException (0으로 나누기), IllegalArgumentException.
  •  

10. Call by Value와 Call by Reference의 차이는?

답변: "두 방식의 가장 큰 차이점은 **'변수의 제어권'**을 어디까지 넘겨주느냐에 있습니다.

  • **Call by Value(값에 의한 호출)**는 메서드 호출 시 인자로 전달되는 변수의 값을 복사하여 전달합니다. 메서드 내부에서는 복사된 값을 사용하기 때문에, 내부에서 값을 아무리 수정해도 메서드 외부의 원본 변수에는 영향을 주지 않습니다.
  • **Call by Reference(참조에 의한 호출)**는 변수가 점유하고 있는 메모리 주소(참조) 그 자체를 전달합니다. 따라서 메서드 내부에서 파라미터를 통해 값을 변경하거나 새로운 대상을 할당하면, 원본 변수도 동시에 변경됩니다.

요약하자면, 복사본을 주느냐(Value), 아니면 원본의 통제권을 공유하느냐(Reference)의 차이입니다."


10-1. 자바(Java)는 어디에 해당하며, 그 이유는?

답변: "자바는 항상 Call by Value로만 동작합니다.

많은 분이 참조형(Object)을 넘길 때 Call by Reference라고 오해하시지만, 자바는 스택(Stack) 메모리에 저장된 '값'을 무조건 복사해서 전달하기 때문입니다.

  1. **기본형(Primitive Type)**은 스택에 저장된 실제 값을 복사해서 넘깁니다.
  2. **참조형(Reference Type)**은 객체가 저장된 힙(Heap) 영역의 주소를 가리키는 **'주소값'**을 복사해서 넘깁니다.

참조형을 넘겼을 때 객체의 내부 필드 값을 바꿀 수 있는 것은 주소라는 복사본을 통해 같은 객체에 접근했기 때문일 뿐입니다. 만약 자바가 Call by Reference였다면 메서드 내부에서 파라미터에 새로운 객체를 할당했을 때 원본 변수도 바뀌어야 하지만, 실제 자바에서는 원본 변수가 변하지 않으므로 Call by Value라고 정의하는 것이 정확합니다."


💡 살짝 다듬어야 할 오개념 2가지

  1. 상속이 필수인가? 오버라이딩은 부모-자식 간의 '상속'이 필수지만, 오버로딩은 상속과 상관없이 같은 클래스 내에서도 자유롭게 사용할 수 있습니다.
  2. 출력(리턴) 타입만 바꿔도 오버로딩인가?
  3. 자바에서는 메서드의 리턴 타입만 바꾸는 것으로는 오버로딩이 되지 않습니다. 반드시 파라미터(입력값)의 개수나 타입, 순서가 달라져야 합니다.

11-1. 오버로딩 (Overloading): "같은 이름으로 여러 개 적재하기"

오버로딩은 **'메서드의 이름은 같지만, 매개변수(파라미터)의 개수나 타입을 다르게 해서 여러 개를 정의하는 것'**입니다.

  • 목적: 같은 기능을 하는 메서드들의 이름을 통일해서 사용성을 높이기 위함입니다. (예: System.out.println()은 정수, 문자열, 객체 등 파라미터가 달라도 모두 같은 이름으로 호출할 수 있죠!)
  • 규칙: 파라미터의 개수, 타입, 순서 중 하나라도 달라야 합니다. 리턴 타입만 다르면 컴파일 에러가 발생합니다.

11-2. 오버라이딩 (Overriding): "부모 메서드 덮어쓰기"

오버라이딩은 **'상속받은 부모 클래스의 메서드를 자식 클래스에서 입맛에 맞게 내부 코드를 재정의(수정)하는 것'**입니다. 말씀하신 "내부 코드를 수정한다"는 표현이 정확합니다.

  • 목적: 부모가 물려준 기능을 자식 클래스의 특성에 맞게 변경해서 사용하기 위함입니다. 객체지향의 핵심인 다형성을 구현하는 중요한 방법입니다.
  • 규칙: 메서드 이름, 파라미터, 리턴 타입이 부모의 것과 100% 똑같아야 합니다.

📊 한눈에 보는 비교 표

구분 오버로딩 (Overloading) 오버라이딩 (Overriding)
개념 새로운 메서드를 정의 (New) 기존 메서드를 덮어쓰기 (Modify)
적용 범위 같은 클래스 내 (상속과 무관) 상속 관계 (부모-자식 간)
메서드 이름 같아야 함 같아야 함
파라미터 달라야 함 (개수, 타입 등) 같아야 함
리턴 타입 상관없음 (파라미터가 다를 경우) 같아야 함

🤔 둘이 같이 쓸 수 있을까?

네, 완벽하게 같이 쓸 수 있습니다! 실무에서도 굉장히 자주 쓰이는 패턴입니다.

예를 들어, 부모 클래스에 sendMessage(String text)라는 메서드가 있다고 해볼게요.

  1. 자식 클래스에서 이 메서드를 상속받아 내부 로직을 수정합니다. (오버라이딩)
  2. 그리고 자식 클래스에 이메일 주소도 같이 받을 수 있는 sendMessage(String text, String email)이라는 메서드를 하나 더 만듭니다. (오버로딩)

 

추가 - 바인딩

바인딩(Binding): 컴퓨터가 "이 메서드를 호출하면 저 코드를 실행해야지!" 하고 연결해 주는 작업이다.

1. 정적 바인딩 (Static Binding)

  • 결정 시점: 프로그램 실행 전, 컴파일(Compile) 타임
  • 특징: 컴파일러가 코드를 번역할 때 변수의 '타입(Type)'만 보고 호출할 메서드를 미리 확정해 버리는 방식이다.
  • 연결되는 것: **오버로딩(Overloading)**이 대표적이다. 메서드 이름이 같아도 파라미터의 개수나 타입이 다르기 때문에, 컴파일러가 코드만 쓱 보고도 "아, 파라미터가 2개인 걸 보니 이 메서드를 부르는 거구나!" 하고 100% 확신할 수 있다. 프로그램 실행 전에 이미 짝이 다 지어져 있으므로 실행 속도가 빠르다. (참고로 자바에서 private, final, static 메서드들도 변경될 여지가 없으므로 정적 바인딩으로 처리된다.)

2. 동적 바인딩 (Dynamic Binding)

  • 결정 시점: 프로그램이 실제로 돌아가는 런타임(Run-time)
  • 특징: 컴파일할 때는 껍데기(참조 변수의 타입)만 보고 대충 연결해 두지만, 프로그램이 실제로 실행되어 메모리에 객체가 생성되었을 때 "어? 실제 들어있는 알맹이는 자식 객체네?" 하고 진짜 객체의 메서드를 찾아가서 연결하는 방식이다.
  • 연결되는 것: **오버라이딩(Overriding)**이 핵심이다. 부모 클래스 타입의 변수에 자식 객체를 담아두었을 때, 겉보기엔 부모 같지만 실행해 보면 자식이 덮어쓴(오버라이딩한) 코드가 실행된다. 이것이 가능한 이유가 바로 프로그램 실행 중에 실제 객체를 확인하고 메서드를 연결해 주는 동적 바인딩 덕분이다. 객체지향의 가장 강력한 무기인 다형성(Polymorphism)을 만들어내는 원리이기도 하다.

[면접용 100점짜리 요약 답변]

"오버로딩은 컴파일 시점에 파라미터를 통해 호출될 메서드가 확정되는 정적 바인딩의 예시입니다. 반면, 오버라이딩은 런타임 시점에 메모리에 할당된 실제 객체의 타입에 따라 호출될 메서드가 결정되는 동적 바인딩의 예시입니다."

 

1. 바인딩(Binding)이란 정확히 무엇인가?

프로그래밍에서 바인딩은 코드에 적힌 '이름(메서드 호출이나 변수명)'을 실제 메모리에 존재하는 '실행할 코드(주소)'와 연결하는 작업이다.

우리가 코드에 user.getName()이라고 적었을 때, 컴퓨터 입장에서는 수많은 메모리 공간 중 도대체 어떤 코드를 실행해야 할지 찾아야 한다. 이 호출부와 실제 구현부를 짝지어 주는 과정이 바로 바인딩이다.

이 짝짓기 작업을 '언제' 하느냐에 따라 정적 바인딩과 동적 바인딩으로 나뉜다.

2. 정적 바인딩 (Static Binding / Early Binding)

  • 결정 시점: 프로그램이 실행되기 전인 컴파일(Compile) 시점
  • 작동 원리: 컴파일러가 코드를 번역할 때, 변수의 '타입(껍데기)'만 보고 "이건 무조건 이 메서드를 실행하는 거네!" 하고 미리 연결을 끝내버린다.
  • 장점: 실행하기 전에 이미 짝이 다 지어져 있으므로 속도가 아주 빠르다. 컴퓨터가 실행 중에 고민할 필요가 없기 때문이다.
  • 해당하는 것들: * 오버로딩(Overloading): 파라미터가 다르면 컴파일러가 이름과 파라미터만 보고도 어떤 메서드인지 100% 확신할 수 있다.
    • static, private, final 메서드: 이 키워드들이 붙은 메서드는 오버라이딩(덮어쓰기)이 불가능하다. 즉, 나중에 코드가 바뀔 가능성이 0%이므로 컴파일러가 안심하고 미리 연결해 둔다.

3. 동적 바인딩 (Dynamic Binding / Late Binding)

  • 결정 시점: 프로그램이 실제로 돌아가고 있는 런타임(Run-time) 시점
  • 작동 원리: 컴파일러가 미리 확정 짓지 않고 보류해 두었다가, 프로그램이 실행되어 메모리에 객체가 생성되었을 때 실제 들어있는 알맹이(객체)를 확인하고 그에 맞는 메서드를 찾아 연결한다.
  • 장점: 실행 속도는 정적 바인딩보다 아주 미세하게 느릴 수 있지만, **다형성(Polymorphism)**이라는 객체지향의 엄청난 유연성을 얻게 된다. 코드를 수정하지 않고도 갈아끼우기만 하면 다른 동작을 하게 만들 수 있다.
  • 해당하는 것들:
    • 오버라이딩(Overriding)된 메서드: 부모 클래스 타입의 변수라도, 나중에 자식 객체를 집어넣으면 실행 시점에 컴퓨터가 "어? 실제 알맹이는 자식이네?" 하고 자식이 덮어쓴 메서드를 실행해 준다. 자바의 일반적인 인스턴스 메서드는 모두 기본적으로 동적 바인딩을 사용한다.

💡 면접 추가 팁

면접관이 "그럼 C언어의 포인터는요?"라고 압박 질문을 던진다면, **"C언어의 포인터 역시 주소라는 '값'을 복사해서 넘기는 것이므로 개념적으로는 Call by Value에 해당하며, 진정한 Call by Reference는 C++의 참조자(&)나 C#의 ref 키워드처럼 변수 자체의 별칭을 넘기는 경우를 말합니다"**라고 답변하시면 완벽합니다.

 

 

다음 영상을 시청하며 진행했습니다

https://www.youtube.com/watch?v=8ezFtSphH2w&t=619s 

많은 이해에 도움을 받은 블로그는 다음과 같습니다.

https://inpa.tistory.com/

 

Inpa Dev 👨‍💻

성장 욕구가 가파른 초보 개발자로서 공부한 내용을 쉽게 풀어쓴 기술 개발자 블로그를 운영하고 있습니다.

inpa.tistory.com