Java 7 이전 핵심 개념 — 객체지향과 컬렉션
Java의 객체지향과 핵심 자료형, 컬렉션 및 예외 처리 개념을 코드 예제로 정리하고 Spring DI 및 백엔드 설계와 연결한다.
목차
- 1. 객체지향 프로그래밍(OOP)
- 1.5 static / 1.6 Wrapper / 1.7 String / 1.8 POJO
- 2. 상속과 다형성
- 3. 추상 클래스와 인터페이스
- 4. 제네릭(Generics)
- 5. 컬렉션 프레임워크
- 6. 예외 처리
- 7. equals와 hashCode
- 8. 개념 간 연결
- 9. 핵심 점검 질문
1. 객체지향 프로그래밍(OOP)
1.1 개념
객체지향 프로그래밍(Object-Oriented Programming)은 상태(필드)와 행동(메서드)을 가진 객체들이 협력하도록 프로그램을 구성하는 방식이다.
public class Account {
private long balance;
public Account(long balance) {
if (balance < 0) throw new IllegalArgumentException("초기 잔액 오류");
this.balance = balance;
}
public void deposit(long amount) {
if (amount <= 0) throw new IllegalArgumentException("입금액 오류");
balance += amount;
}
public long getBalance() {
return balance;
}
}- 클래스: 객체의 구조와 행동을 정의하는 설계도.
- 객체/인스턴스: 클래스를 기반으로 생성되어 독립적인 상태를 가진 대상.
- 캡슐화: 데이터와 행동을 묶어 객체가 자신의 상태를 관리하게 한다.
- 정보 은닉:
private등의 접근 제어자로 내부 구현에 대한 직접 접근을 제한한다. 캡슐화와 정보 은닉은 연관되지만 같은 뜻은 아니다.
1.2 객체지향의 네 가지 특징
| 특징 | 의미 | 예시 |
|---|---|---|
| 캡슐화(Encapsulation) | 상태와 행동을 한 단위로 묶어 관리 | Account.deposit() |
| 상속(Inheritance) | 기존 클래스의 특성을 확장 | Dog extends Animal |
| 다형성(Polymorphism) | 같은 상위 타입으로 다양한 구현을 사용 | PaymentProcessor |
| 추상화(Abstraction) | 핵심 계약을 노출하고 구현 세부사항을 감춤 | interface |
1.3 접근 제어자
| 제어자 | 동일 클래스 | 동일 패키지 | 타 패키지 하위 클래스 | 외부 일반 클래스 |
|---|---|---|---|---|
private | O | X | X | X |
| 기본(package-private) | O | O | X | X |
protected | O | O | O* | X |
public | O | O | O | O |
* protected의 타 패키지 접근은 상속 관계의 접근 조건을 따른다.
1.4 SOLID 원칙
| 약어 | 정식 명칭 | 핵심 의미 |
|---|---|---|
| SRP | 단일 책임 원칙(Single Responsibility Principle) | 클래스/모듈은 하나의 변경 이유에 집중한다. |
| OCP | 개방-폐쇄 원칙(Open-Closed Principle) | 확장에는 열려 있고 기존 코드 수정에는 닫혀 있도록 설계한다. |
| LSP | 리스코프 치환 원칙(Liskov Substitution Principle) | 하위 타입으로 대체해도 상위 타입의 기대 동작이 유지되어야 한다. |
| ISP | 인터페이스 분리 원칙(Interface Segregation Principle) | 사용하지 않는 메서드에 의존하도록 강요하지 않는다. |
| DIP | 의존성 역전 원칙(Dependency Inversion Principle) | 상위 정책과 하위 구현 모두 구체 클래스보다 추상화에 의존한다. |
실무 연결: Spring에서 인터페이스에 의존하고 구현체를 주입받는 구조는 DIP를 실천하는 대표적인 방법이다. DI를 사용했다고 자동으로 SOLID가 지켜지는 것은 아니다.
1.5 static 키워드
static 멤버는 객체마다 따로 존재하는 것이 아니라 클래스 단위로 사용된다. 인스턴스 필드와 달리 클래스의 여러 인스턴스가 같은 static 필드를 공유한다.
class Counter {
static int totalCount = 0; // 모든 인스턴스가 공유
int instanceCount = 0; // 인스턴스마다 별도 보유
Counter() {
totalCount++;
instanceCount++;
}
static int getTotalCount() {
return totalCount;
}
}
Counter first = new Counter();
Counter second = new Counter();
System.out.println(Counter.getTotalCount()); // 2
System.out.println(first.instanceCount); // 1
System.out.println(second.instanceCount); // 1- static 필드: 클래스 단위로 공유한다. 변경 가능한 static 상태는 동시성 문제에 유의해야 한다.
- static 메서드: 인스턴스 없이 클래스명으로 호출할 수 있으며,
this나 인스턴스 필드에 직접 접근할 수 없다. - static 초기화 블록: 클래스 초기화 과정에서 실행된다(동일 클래스 로더로 정의한 클래스 기준 일반적으로 한 번).
- static 메서드 숨김: 하위 클래스에서 같은 시그니처의 static 메서드를 선언해도 오버라이딩과 달리 컴파일 타임 타입을 기준으로 선택한다. 자세한 내용은 2.2 다형성과 동적 디스패치를 참고한다.
static은 단순히 “클래스 로딩 시 Method Area에 저장된다”라고 외우기보다 클래스 초기화 시점과 JVM의 논리적 런타임 데이터 영역을 구분해서 이해하는 편이 정확하다.
1.6 Wrapper Class와 오토박싱
Wrapper Class는 int, long, boolean 같은 기본형(primitive)을 Integer, Long, Boolean 같은 객체 타입으로 다루기 위한 클래스다. 제네릭 타입 인자로 기본형을 쓸 수 없기 때문에 컬렉션에서 자주 사용한다.
| 기본형 | Wrapper Class |
|---|---|
int | Integer |
long | Long |
double | Double |
boolean | Boolean |
char | Character |
Integer boxed = 10; // 오토박싱: int → Integer
int value = boxed; // 언박싱: Integer → int
List<Integer> numbers = new ArrayList<Integer>();
numbers.add(10); // 오토박싱주의할 점:
- 언박싱 시 NPE:
Integer count = null; int n = count;는NullPointerException이 발생한다. ==주의: 두 Wrapper 객체 사이의==는 참조 동일성을 비교한다. 숫자 값 비교에는equals()또는 언박싱 후 기본형 비교가 적합하다.- Integer 캐시:
Integer.valueOf()는 최소-128부터127까지 캐시를 이용하므로 일부 값에서 참조가 우연히 같아질 수 있다. 캐시 여부를 값 비교의 근거로 사용하면 안 된다. - 성능: 반복 연산에서 불필요한 박싱/언박싱은 객체 생성 및 성능 비용을 만들 수 있다.
1.7 String, StringBuilder, StringBuffer
String은 **불변(immutable)**이다. 문자열을 결합하거나 변형한 결과는 기존 문자열을 수정하는 것이 아니라 새 문자열 값으로 표현한다. StringBuilder와 StringBuffer는 내용을 수정할 수 있는 가변 문자열 도구다.
| 항목 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 가변성 | 불변 | 가변 | 가변 |
| 변경 API | 변경 결과로 새 String을 얻음 | append(), insert() 등 | append(), insert() 등 |
| 동기화 | 불변이므로 상태 변경 없음 | 동기화 제공하지 않음 | 주요 메서드 동기화 |
| 일반적 선택 | 문자열 값·식별자 | 단일 흐름에서 반복 결합 | 공유 가변 버퍼의 동기화가 필요할 때 신중히 선택 |
String text = "Java";
text.concat(" 7");
System.out.println(text); // Java (원래 문자열 그대로)
text = text.concat(" 7");
System.out.println(text); // Java 7
StringBuilder builder = new StringBuilder("Java");
builder.append(" 7");
System.out.println(builder); // Java 7문자열 연결이 언제나 새 객체를 연속으로 만드는 것은 아니다. 컴파일 시 상수 결합이나 JVM 최적화가 적용될 수 있다. 성능이 중요할 때는 실제 반복 패턴을 기준으로 판단해야 한다. 또한 StringBuffer의 개별 메서드가 동기화되어 있다고 해서 여러 호출을 결합한 전체 작업이 자동으로 원자적이 되는 것은 아니다.
1.8 POJO (Plain Old Java Object)
POJO는 특정 프레임워크의 기반 클래스를 상속하거나 특정 프레임워크 API에 강하게 결합되지 않은 일반적인 Java 객체를 가리키는 용어다.
public class Order {
private final long id;
public Order(long id) { this.id = id; }
public long getId() { return id; }
}- POJO ≠ JavaBean: JavaBean은 관례적 프로퍼티 접근 메서드와 기본 생성자 등 별도의 관례를 가진다. 모든 POJO가 JavaBean인 것은 아니다.
- POJO ≠ Spring Bean: Spring Bean은 Spring 컨테이너가 관리하는 객체를 뜻한다. 일반 POJO를 Spring Bean으로 등록할 수 있다.
- 실무 의미: 순수 Java 객체에 비즈니스 규칙을 담으면 프레임워크 의존을 줄이고 독립적으로 테스트하기 쉬워진다.
2. 상속과 다형성
2.1 상속과 오버라이딩
class Animal {
String name = "Animal";
void sound() {
System.out.println("동물 소리");
}
static void info() {
System.out.println("Animal 정보");
}
}
class Dog extends Animal {
String name = "Dog";
@Override
void sound() {
System.out.println("멍멍");
}
static void info() {
System.out.println("Dog 정보");
}
}상속(extends)은 부모의 기능을 하위 클래스에서 확장하는 방식이다. Java 클래스는 직접적인 부모 클래스를 하나만 가질 수 있다.
2.2 다형성과 동적 디스패치
Animal animal = new Dog();
animal.sound(); // 멍멍
System.out.println(animal.name); // Animal
animal.info(); // Animal 정보
Dog.info(); // Dog 정보- 참조 변수의 선언 타입은
Animal, 실제 생성한 객체 타입은Dog이다. - 인스턴스 메서드
sound(): 실제 객체가Dog이므로Dog.sound()를 호출한다. 이렇게 실행할 오버라이딩 메서드가 런타임의 실제 객체 타입에 따라 정해지는 것이 **동적 디스패치(Dynamic Dispatch)**다. - 필드
name: 오버라이딩되지 않는다.animal.name은 참조 변수에 선언된 타입Animal기준으로 선택된다.Animal.name과Dog.name은 서로 다른 필드다. static메서드info(): 객체가 아닌 클래스에 속한다. 재선언하면 오버라이딩이 아니라 **메서드 숨김(Method Hiding)**이다. 호출 대상이 컴파일 타임 타입에 따라 선택되므로 혼란을 막으려면Animal.info()처럼 클래스명으로 호출한다.
| 대상 | 선택 기준 | 위 예시 결과 |
|---|---|---|
| 오버라이딩 가능한 인스턴스 메서드 | 런타임 실제 객체 타입 | 멍멍 |
| 인스턴스 필드 | 참조 변수의 선언 타입 | Animal |
static 메서드 | 컴파일 타임 타입 또는 명시된 클래스 | Animal 정보 |
암기: 오버라이딩된 인스턴스 메서드는 실제 객체를 따라간다. 필드와 static 메서드는 선언된 타입을 기준으로 선택된다.
2.3 오버로딩 vs 오버라이딩
| 구분 | 오버로딩(Overloading) | 오버라이딩(Overriding) |
|---|---|---|
| 조건 | 같은 이름, 다른 매개변수 목록 | 상속된 인스턴스 메서드를 재정의 |
| 상속 필요 | X | O |
| 메서드 선택 | 컴파일 시점 | 런타임 실제 객체 타입 |
| 반환 타입 | 반환 타입만 달리해 구별할 수 없음 | 같거나 공변 반환 타입 허용 |
2.4 상속보다 조합(Composition)을 고려하는 이유
상속은 부모가 가진 구현에 자식이 결합되기 쉽다. 부모의 결제 한도 검증 규칙을 고쳤는데, 이를 상속한 카드/계좌이체 서비스가 모두 영향을 받는 상황을 생각해 볼 수 있다.
조합은 필요한 기능을 가진 객체를 **필드로 보유(has-a)**하고 작업을 위임한다. 검증 규칙을 별도 정책 객체로 분리하면 수단별 정책 교체가 쉬워진다.
interface PaymentValidator {
void validate(long amount);
}
class CardValidator implements PaymentValidator {
@Override
public void validate(long amount) {
if (amount <= 0 || amount > 1_000_000) {
throw new IllegalArgumentException("카드 결제 한도 초과");
}
}
}
class BankValidator implements PaymentValidator {
@Override
public void validate(long amount) {
if (amount <= 0 || amount > 10_000_000) {
throw new IllegalArgumentException("계좌이체 한도 초과");
}
}
}
class PaymentService {
private final PaymentValidator validator;
PaymentService(PaymentValidator validator) {
this.validator = validator;
}
void pay(long amount) {
validator.validate(amount);
System.out.println("결제 처리");
}
}
PaymentService card = new PaymentService(new CardValidator());
PaymentService bank = new PaymentService(new BankValidator());
card.pay(500_000);
bank.pay(5_000_000);| 비교 | 상속 | 조합 |
|---|---|---|
| 관계 | is-a | has-a |
| 재사용 방식 | 부모 구현 상속 | 외부 객체에 기능 위임 |
| 주요 위험 | 부모 구현 변경이 하위 클래스에 전파 | 구현체를 직접 고정하면 여전히 결합됨 |
| 실무 설계 | 안정적인 상위-하위 타입 관계에 적합 | 정책 변경·구현체 교체에 유리 |
핵심: 조합만으로 자동으로 느슨한 결합이 생기는 것은 아니다. **조합 + 인터페이스 + 의존성 주입(DI)**을 함께 사용하면 구체 구현에 대한 의존을 줄일 수 있다.
3. 추상 클래스와 인터페이스
3.1 추상 클래스
공통 상태와 구현을 제공하고, 일부 동작을 하위 클래스에서 구현하도록 할 때 사용한다.
abstract class Payment {
protected final long amount;
Payment(long amount) {
this.amount = amount;
}
void validate() {
if (amount <= 0) throw new IllegalArgumentException();
}
abstract void execute();
}- 직접 인스턴스화할 수 없지만 생성자와 인스턴스 필드는 가질 수 있다.
- 일반 메서드와 추상 메서드를 함께 가질 수 있다.
- 추상 메서드가 없어도
abstract class로 선언할 수 있다.
3.2 인터페이스
구현체가 따라야 하는 기능 계약을 정의한다.
interface PaymentProcessor {
void process(long amount);
}
class CardProcessor implements PaymentProcessor {
@Override
public void process(long amount) {
System.out.println("카드 결제: " + amount);
}
}
class BankProcessor implements PaymentProcessor {
@Override
public void process(long amount) {
System.out.println("계좌이체: " + amount);
}
}| 항목 | 추상 클래스 | 인터페이스 |
|---|---|---|
| 주목적 | 공통 기반 구현 및 상태 제공 | 기능 계약 제공 |
| 생성자 / 인스턴스 상태 | 가능 | 불가능 |
| 다중 사용 | 클래스 상속은 하나만 | 여러 인터페이스 구현 가능 |
| Java 7 일반 구현 메서드 | 가능 | 불가능 |
버전 구분: 인터페이스 default/static 메서드는 Java 8, private 메서드는 Java 9부터 지원한다.
3.3 Spring DI와 연결하기
class PaymentService {
private final PaymentProcessor processor;
PaymentService(PaymentProcessor processor) {
this.processor = processor;
}
void pay(long amount) {
processor.process(amount);
}
}
PaymentService service = new PaymentService(new CardProcessor());
service.pay(10_000); // 카드 결제PaymentService는 CardProcessor라는 구체 클래스 대신 PaymentProcessor라는 인터페이스에 의존한다. 이 코드는 일반 Java 생성자 주입이며, Spring 없이도 가능한 DI다.
Spring을 사용한다면 구현체를 Bean으로 등록하고 컨테이너가 생성자에 전달한다.
@Component("cardProcessor")
class CardProcessor implements PaymentProcessor {
@Override
public void process(long amount) {
System.out.println("카드 결제: " + amount);
}
}
@Component("bankProcessor")
class BankProcessor implements PaymentProcessor {
@Override
public void process(long amount) {
System.out.println("계좌이체: " + amount);
}
}
@Service
class PaymentService {
private final PaymentProcessor processor;
PaymentService(@Qualifier("cardProcessor") PaymentProcessor processor) {
this.processor = processor;
}
void pay(long amount) {
processor.process(amount);
}
}위 예시는 Spring 환경의 독립적인 코드 조각이다. @Component, @Service, @Qualifier 및 관련 import가 필요하다.
- 구현 Bean이 둘이라면
@Qualifier등으로 주입 대상을 선택해야 한다. - 위 코드는 항상 카드 처리기를 사용하는 형태다. 요청별로 결제수단을 선택하려면 여러 전략을 주입받아 요청에 따라 선택하는 팩토리/레지스트리 방식 등을 사용해야 한다.
- 연결: 인터페이스(계약) → 조합(필드 보유) → DI(외부 주입) → 다형성/동적 디스패치(실제 구현 실행).
4. 제네릭(Generics)
4.1 사용 목적
타입을 매개변수로 받아 컴파일 시점에 타입 안전성을 확보하고 형변환을 줄인다.
List<String> names = new ArrayList<String>();
names.add("Java");
// names.add(100); // 컴파일 오류
String first = names.get(0);Java 7부터 new ArrayList<>()처럼 다이아몬드 연산자로 타입 인자를 생략할 수 있다.
4.2 제네릭 클래스 / 메서드
class Box<T> {
private T value;
void set(T value) { this.value = value; }
T get() { return value; }
}
static <T> T first(List<T> values) {
return values.get(0);
}관례: T(Type), E(Element), K(Key), V(Value).
4.3 와일드카드와 PECS
| 형태 | 뜻 | 주된 사용 |
|---|---|---|
List<?> | 원소 타입 미상 | 타입 무관 조회 |
List<? extends Number> | Number 또는 하위 타입 | 읽기(Producer) |
List<? super Integer> | Integer 또는 상위 타입 | 쓰기(Consumer) |
PECS: Producer Extends, Consumer Super.
extends컬렉션에는 일반적으로 새 값을 추가할 수 없고(null은 예외),super컬렉션에서 읽으면 안전하게는Object로 받는다.
4.4 타입 소거(Type Erasure)
제네릭 타입 인자는 대부분 컴파일 과정에서 소거되어 런타임의 인스턴스 클래스가 같아진다.
List<String> a = new ArrayList<String>();
List<Integer> b = new ArrayList<Integer>();
System.out.println(a.getClass() == b.getClass()); // true제약 사항: new T(), new T[10], instanceof List<String>은 직접 사용할 수 없다. 기본형은 타입 인자가 될 수 없으므로 List<Integer>를 사용한다. 일부 제네릭 선언 정보는 클래스 메타데이터에 남을 수 있다.
5. 컬렉션 프레임워크
5.1 핵심 구분
| 인터페이스 | 특징 | 대표 구현체 |
|---|---|---|
List | 순서 있음, 중복 허용 | ArrayList, LinkedList |
Set | 중복 불허 | HashSet, TreeSet |
Queue | 대기열/우선순위 처리 | ArrayDeque, PriorityQueue |
Map | Key-Value, Key 중복 불허 | HashMap, TreeMap |
List, Set, Queue는 Collection의 하위 인터페이스이며 Map은 별도 인터페이스다.
5.2 ArrayList와 LinkedList
| 연산 | ArrayList | LinkedList |
|---|---|---|
| 인덱스 조회 | O(1) | O(n) |
| 끝에 추가 | 평균 O(1) | O(1) |
| 중간 삽입 | 이동 때문에 O(n) | 위치 탐색 O(n), 연결 변경 O(1) |
| 메모리/지역성 | 참조 배열, 접근 지역성 유리 | 노드 객체/참조 오버헤드 |
일반적인 목록은 ArrayList를 먼저 고려하고, LinkedList가 유리한지는 실제 접근 패턴으로 판단한다.
5.3 HashMap 작동 원리
- Key의
hashCode()를 호출한다. - 해시를 가공해 버킷 위치를 계산한다.
- 버킷에 저장된 항목과 해시·
equals()를 비교한다. - 동일 Key면 Value를 교체하고, 없으면 항목을 추가한다.
- 해시 충돌: 서로 다른 Key가 동일 버킷을 사용하는 현상.
- 일반적인
get/put의 기대 시간복잡도는 **O(1)**이며 충돌이나 리사이징으로 비용이 증가할 수 있다. - 버전 주의: 충돌 버킷을 조건부 트리화하는 최적화는 Java 8 이후의
HashMap구현이다. Java 7 시점의 설명과 구분할 것. HashSet도 내부적으로 해시 기반 Map 구조를 사용하므로 동등성 규약이 중요하다.
5.4 HashMap / Hashtable / ConcurrentHashMap
| 구분 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 스레드 안전성 | 기본적으로 없음 | 메서드 동기화 | 동시성 처리 지원 |
| null Key / Value | 허용(단일 null Key) | 불허 | 불허 |
| 사용 | 일반적인 Map | 레거시 | 공유 Map 동시 접근 |
주의: ConcurrentHashMap이어도 get 후 put 같은 여러 작업이 하나의 원자적 연산으로 바뀌는 것은 아니다. 필요에 따라 putIfAbsent, compute 등을 사용한다. 내부 구현 방식은 Java 버전에 따라 다르다.
6. 예외 처리
6.1 예외 계층
Throwable
├── Error (unchecked)
└── Exception
├── IOException (checked)
├── SQLException (checked)
└── RuntimeException (unchecked)
├── NullPointerException
└── IllegalArgumentException| 구분 | 특징 | 예시 |
|---|---|---|
| Checked Exception | 컴파일러가 처리 또는 throws 선언을 요구 | IOException, SQLException |
| Unchecked Exception | 컴파일러가 처리를 강제하지 않음 | RuntimeException, Error 계열 |
| Error | 보통 애플리케이션 수준에서 복구하기 어려운 문제 | OutOfMemoryError |
6.2 throw, throws, finally
throw: 예외 객체를 실제로 발생시킴.throws: 메서드가 전달할 수 있는 예외를 선언.finally: 일반적인 실행/예외 흐름에서 정리 코드 실행. JVM 강제 종료 등에서는 실행이 보장되지 않음.
6.3 try-with-resources (Java 7)
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
System.out.println(reader.readLine());
} catch (IOException e) {
e.printStackTrace();
}AutoCloseable 리소스를 자동으로 닫으며, 처리 중 예외와 종료 중 예외가 같이 나면 후자는 suppressed exception으로 기록될 수 있다.
6.4 커스텀 예외와 Spring 연결
class PaymentException extends RuntimeException {
PaymentException(String message, Throwable cause) {
super(message, cause);
}
}
try {
// 외부 결제 시스템 호출
} catch (IOException e) {
throw new PaymentException("결제 연동 실패", e);
}예외를 변환할 때 cause를 보존해야 장애 원인을 추적하기 좋다. 위 예시는 예외 처리 구조를 보여주는 코드 조각이다.
Spring 트랜잭션 기본 규칙: 기본적으로 RuntimeException과 Error는 롤백 대상이고, Checked Exception은 기본 롤백 대상이 아니다. rollbackFor 등으로 규칙을 조정할 수 있다.
7. equals와 hashCode
7.1 ==와 equals()
- 기본형에 대한
==: 값 비교. - 참조형에 대한
==: 같은 객체를 참조하는지 비교. equals(): 기본Object구현은 참조 동일성 비교지만, 오버라이딩하면 논리적 동등성 비교가 가능.
String a = new String("Java");
String b = new String("Java");
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true7.2 두 메서드를 함께 재정의하는 이유
class Member {
private final long id;
private final String name;
Member(long id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Member)) return false;
Member other = (Member) obj;
return id == other.id;
}
@Override
public int hashCode() {
return Long.hashCode(id);
}
}여기서는 ID가 같으면 동일 회원이라는 도메인 규칙을 가정했다. name은 동등성 판단에서 제외한다.
equals() 규약: 반사성, 대칭성, 추이성, 일관성, null 비교 시 false.
hashCode() 규약: equals()가 true면 해시 값이 반드시 같아야 한다. 해시 값이 같다고 equals()가 true일 필요는 없다.
Set<Member> members = new HashSet<Member>();
members.add(new Member(1L, "Kim"));
members.add(new Member(1L, "Lee"));
System.out.println(members.size()); // 1equals()만 재정의하고 hashCode()를 재정의하지 않으면 해시 기반 컬렉션에서 논리적 중복 판단이 깨질 수 있다.
7.3 가변 Key 문제
HashMap에 넣은 Key의 동등성/해시 계산에 사용되는 필드를 변경하면, 저장할 때의 버킷과 조회할 때의 버킷이 달라져 조회에 실패할 수 있다. 그러므로 Map Key는 가능한 불변 객체를 사용한다.
8. 개념 간 연결
인터페이스로 계약 정의
↓
PaymentService가 PaymentProcessor를 필드로 보유 (조합)
↓
생성자 또는 Spring 컨테이너가 구현 객체를 전달 (DI)
↓
processor.process() 호출
↓
실제 주입된 CardProcessor / BankProcessor 메서드 실행 (다형성/동적 디스패치)| 기본 개념 | 실무 연결 |
|---|---|
| OOP/캡슐화 | 결제 상태 및 규칙을 객체 안에서 관리 |
| 다형성/인터페이스 | PG·결제수단별 구현을 동일 계약으로 호출 |
| 조합/DI | 서비스 내부에서 구체 구현을 직접 생성하지 않음 |
| 제네릭 | 타입 안전한 API 응답·공통 유틸리티 |
| 컬렉션 | 거래 목록과 Key 기반 조회 구조 |
| 예외 | 장애 원인 보존·업무 예외 분리·트랜잭션 정책 |
| equals/hashCode | 회원/거래 값 객체의 논리적 동등성 및 해시 자료구조 |
9. 핵심 점검 질문
- 객체지향의 네 가지 특징을 결제 서비스 예제로 설명해 보자.
- SOLID의 한글명·영문명과 각 원칙의 핵심 취지는 무엇인가?
Animal a = new Dog()에서 오버라이딩 메서드는 왜Dog의 메서드가 실행되는가?- 같은 코드에서 필드와
static메서드는 왜 다른 결과가 나오는가? - 오버로딩과 오버라이딩은 어떻게 다른가?
- 상속과 조합의 차이, 조합이 유리한 경우는 무엇인가?
- 추상 클래스와 인터페이스는 어떤 기준으로 선택하는가?
- 일반 생성자 주입과 Spring DI의 차이는 무엇인가?
- 인터페이스 구현체가 두 개일 때 Spring에서 어떻게 선택하는가?
- 제네릭의 타입 소거와 PECS는 무엇인가?
- ArrayList와 LinkedList의 삽입·조회 비용은 어떻게 다른가?
- HashMap의 저장/조회 및 충돌 처리 과정을 설명해 보자.
- Checked Exception과 Unchecked Exception의 차이는 무엇인가?
equals()를 재정의할 때hashCode()도 재정의하는 이유는 무엇인가?- 가변 객체를 HashMap Key로 사용하면 무엇이 위험한가?