객체지향 5원칙 — 클래스·캡슐화·상속·다형성·인터페이스
객체지향 5원칙 — 클래스·캡슐화·상속·다형성·인터페이스
🎯 이 lesson 을 읽고 나면
이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.
- ▸✅ 캡슐화 · 상속 · 다형성 · 추상화 정의 + 코드 예시
- ▸✅ SOLID 5원칙 (특히 SRP·OCP·DIP) 면접 답변
- ▸✅ 왜 상속 < 컴포지션 인지 한 줄 설명
학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.
객체지향이 뭐냐 — *현실의 사물처럼* 코드를 짜는 방식
핵심 한 줄
객체지향 (OOP) = 현실의 사물을 객체로 표현하고, 그 객체들이 서로 메시지를 주고받으며 일을 처리하는 프로그래밍 방식. Java 의 가장 중심 사상 입니다.
왜 객체지향인가
옛날 C 언어는 함수와 데이터가 따로 였습니다. 사람 의 이름·나이는 변수로, 걷기·말하기 는 함수로 — 분리돼서 큰 프로그램이 되면 뭐가 뭔지 모르게 됐죠.
객체지향은 데이터와 행동을 한 덩어리로 묶어서 다룹니다. "사람" 이라는 클래스 안에 이름·나이 같은 필드 와 걷기·말하기 같은 메서드 가 함께 있습니다. 마치 현실의 사람이 몸 (데이터) 과 행동 을 동시에 가진 것처럼요.
클래스 vs 객체 — 붕어빵 틀과 붕어빵
가장 헷갈리는 두 단어입니다. 비유로 풀어볼게요.
- ▸클래스 = 붕어빵 틀. 설계도. "이런 모양의 붕어빵을 만들 거야" 라는 청사진.
- ▸객체 = 실제로 만들어진 붕어빵. 1개·2개·100개 만들 수 있고, 각각 독립적입니다.
new 키워드가 붕어빵을 굽는 행위 입니다. 메모리 어딘가에 새 공간을 잡고 그 위치를 a 라는 변수에 기록합니다.
캡슐화 — 외부에서 함부로 못 만지게
내 통장 잔액을 아무나 마음대로 바꿀 수 있다면? 큰일 나죠. 그래서 객체지향에서는 외부 직접 접근을 막고 정해진 메서드를 통해서만 데이터를 다루도록 합니다.
private 키워드로 직접 접근을 차단 하고, 입금() 같은 공식 통로 만 열어둡니다. 그 통로 안에서 검증·로깅·동기화 같은 부가 작업을 다 처리할 수 있어요. 이게 캡슐화의 핵심입니다.
상속 — 공통 부분을 재사용
여러 종류의 동물 클래스를 만든다고 해봅시다. 강아지·고양이·새 — 모두 움직이고 먹습니다. 이걸 매번 따로 쓰면 중복입니다.
강아지는 동물의 모든 기능 을 자동으로 물려받고, 자기만의 행동 만 추가하면 됩니다.
다만 상속은 무서운 무기 입니다. 깊게 쌓으면 부모 한 줄 바꾸면 자식 모두 영향 — 변경이 어려워집니다. 그래서 현대 Java 에서는 합성 (composition) 을 선호합니다. 상속보다 포함 — 강아지가 동물을 상속 하는 게 아니라, 강아지가 Movable·Eatable 인터페이스 를 가지는 방식이죠.
다형성 — 같은 인터페이스, 다른 동작
같은 메서드 호출이 객체에 따라 다른 결과 를 내는 것이 다형성입니다.
a 와 b 모두 동물 타입이지만, 소리() 메서드는 실제 객체의 타입 에 따라 다르게 동작합니다. 런타임 에 결정되는 거죠.
이 덕분에 List<동물> 한 리스트에 강아지·고양이를 섞어 담고 반복문 하나 로 처리할 수 있습니다. 동작은 각자 알아서 합니다.
인터페이스 vs 추상 클래스 — 자주 혼동되는 둘
둘 다 직접 생성 못 하는 추상 타입입니다. 차이는:
- ▸인터페이스 — 역할. "비교할 수 있다 (Comparable)"·"반복할 수 있다 (Iterable)" 같은 능력 명세. 다중 구현 가능 (
implements A, B, C). - ▸추상 클래스 — 공통 골격. "이런 메서드들은 다 구현했지만 일부는 자식이 채워야 함". 단일 상속만 (
extends A).
규칙: 역할만 정의 하고 싶으면 인터페이스, 공통 코드 + 일부 추상 이면 추상 클래스. Java 8 부터 인터페이스도 default 메서드로 구현을 가질 수 있어 경계가 흐려졌지만, 역할 vs 골격 의 직관은 여전히 유효합니다.
한 번 정리
객체지향은 복잡한 프로그램을 작은 단위 (클래스) 로 쪼개서 관리 하는 방식입니다. 핵심 5원칙:
> 💡 실무 현장: Java 백엔드는 95% OOP. 함수형·반응형은 보조 도구. OOP 가 가장 기본 무기입니다.
예외 처리 — 에러를 *우아하게* 다루는 법
예외가 뭐냐
프로그램이 예상 못 한 상황 에 부딪혔을 때 — 파일이 없다·DB 연결이 끊겼다·0 으로 나눴다 — Java 는 예외 (Exception) 라는 객체를 던집니다 (throw). 누군가 받지 (catch) 않으면 프로그램이 터집니다.
옛 C 처럼 에러 코드를 리턴 하지 않고 별도 경로로 에러를 전파 하는 메커니즘이죠.
두 종류의 예외 — Checked vs Unchecked
이게 Java 의 특이한 점 입니다. 다른 언어에는 거의 없는 구분.
- ▸Checked: 컴파일러가 반드시 처리하라고 강제.
IOException·SQLException등. try-catch 또는 throws 선언 필수. - ▸Unchecked (RuntimeException): 처리 권장이지만 강제 X.
NullPointerException·IllegalArgumentException등.
이 구분은 오랫동안 논쟁 입니다. Spring·최근 라이브러리는 Checked 를 거의 안 씁니다 — 강제 처리가 코드를 더럽힌다고 보거든요. Kotlin·C# 은 아예 Unchecked 만 있습니다.
try-catch-finally
finally 는 자원 정리에 썼지만, try-with-resources (Java 7+) 가 더 깔끔합니다:
AutoCloseable 을 구현한 자원은 블록 끝나면 자동 close. DB 연결·파일·네트워크 소켓 모두 이 패턴이 표준입니다.
흔한 함정 4가지
1. catch (Exception e) { } — 빈 catch: 가장 무서운 안티패턴. 에러를 조용히 삼킵니다. 디버깅 지옥의 시작. 최소한 로그라도 남기세요.
2. e.printStackTrace(): 표준 출력으로 stack trace 가 흩어집니다. production 에서는 logger 로:
3. 한 줄에 두 가지 일: try 블록 안에서 서로 다른 종류의 작업 을 묶으면 어디서 에러났는지 모릅니다. 작은 try 여러 개가 낫습니다.
4. 도메인 의미를 살리지 않은 예외: throw new RuntimeException("error") 보다 throw new InsufficientBalanceException() 같은 의미 있는 이름 이 디버깅·문서에 큰 차이를 만듭니다.
한 번 정리
예외 처리는 방어 코드 도배 가 아닙니다. 정상 흐름 vs 예외 흐름 을 명확히 분리하고, 의미 있는 메시지 + 로깅 으로 추후 추적 가능하게 만드는 게 핵심입니다.
제네릭 — *타입을 변수처럼* 다루기
왜 제네릭이 필요한가
Java 5 이전엔 List 가 모든 타입을 담을 수 있었습니다. 편리해 보이지만 런타임에 ClassCastException 으로 폭발하는 사고가 잦았죠.
제네릭 은 컴파일 타임에 타입을 검증 하는 장치입니다.
런타임 폭발 → 컴파일러가 미리 잡습니다. 타입 안전성 이 핵심 가치입니다.
기본 사용법
<T> 는 타입 매개변수 — 함수의 매개변수와 비슷한 개념. 컴파일러가 호출 시점에 어떤 타입인지 추론합니다.
Bounded Type — T 의 조건
<T extends Number> = T 는 Number 또는 그 자손 만 가능. Integer·Long·Double 같은 숫자 타입만 통과.
Wildcard — ? 의 의미
가장 헷갈리는 부분입니다.
- ▸
List<? extends Number>— Number 의 어떤 자손 (정확히 모름). 읽기만 가능, 쓰기 X. - ▸
List<? super Integer>— Integer 의 어떤 부모 (정확히 모름). 쓰기는 가능 (Integer 만), 읽기는 Object 로만.
PECS 원칙 — Producer Extends, Consumer Super:
- ▸데이터를 꺼내 쓰는 곳 (생산자) →
extends - ▸데이터를 넣기만 하는 곳 (소비자) →
super
> 외울 필요 없음. 제네릭 라이브러리 만들 때 만 깊이 봅니다. 사용자 입장에선 List<User> 같은 단순 사용이 99%.
Type Erasure — 런타임엔 사라진다
Java 제네릭의 기묘한 특성. 컴파일 후엔 모든 <T> 가 사라지고 Object 로 변환됩니다. 그래서:
- ▸
new T()불가 (런타임에 T 가 뭔지 모름) →Class<T>인자로 받아야 - ▸
T[]배열 생성 불가 - ▸
instanceof List<String>불가 →instanceof List<?>만
이게 Java 가 한 번 결정해서 못 되돌리는 디자인 부채 입니다. C# 의 Reified Generics 처럼 됐으면 더 좋았겠지만, 호환성 때문에 못 바꿉니다.
한 번 정리
제네릭은 컴파일 타임 타입 안전 을 위한 도구입니다. List<User> 같은 기본 사용은 반드시. Wildcard·Bounded 는 라이브러리 작성 시 깊이 들어가면 됩니다.
enum + 어노테이션 — Java 의 *메타 도구*
enum — 그냥 상수가 아니다
다른 언어에선 enum 이 단순 상수 모음 입니다. Java 의 enum 은 훨씬 강력 — 각 값이 객체 이고, 메서드·필드·인터페이스 구현 까지 가능합니다.
각 enum 값에 추가 정보 를 가질 수 있고, 메서드도 정의할 수 있습니다. 상태 머신·Strategy 패턴 에 자주 씁니다.
if-else 도배 대신 각 enum 이 자기 행동 을 갖는 패턴입니다.
어노테이션 — 코드에 메타데이터 붙이기
@Override·@Deprecated·@SuppressWarnings — 이 @ 로 시작하는 게 모두 어노테이션입니다. 코드 자체 가 아니라 코드에 대한 정보 를 표시합니다.
Spring 의 핵심은 어노테이션
Java 백엔드를 한다면 어노테이션이 코드의 80% 를 차지합니다. Spring 의 거의 모든 마법은 어노테이션 기반:
- ▸
@Component·@Service·@Repository— 빈 등록 - ▸
@Autowired— 의존성 주입 - ▸
@RestController+@GetMapping("/users")— 웹 라우팅 - ▸
@Transactional— 트랜잭션 자동 관리 - ▸
@Entity+@Id— JPA 매핑
어노테이션 그 자체 는 아무것도 안 합니다. Spring 같은 프레임워크가 런타임에 어노테이션을 읽고 그에 맞는 동작 (라우팅·DI·트랜잭션) 을 자동으로 수행합니다.
한 번 정리
- ▸enum = 타입 안전한 상수 + 각자 객체처럼 메서드·필드 가능
- ▸어노테이션 = 메타데이터. 직접 실행 X. 프레임워크·도구가 읽고 동작
이 둘이 Java 의 선언형 (declarative) 스타일을 가능하게 합니다. "이렇게 동작해라" 명령이 아니라 "이런 것이다" 선언만 하면 도구가 알아서.
SOLID 5원칙 — bad / good 코드로 비교
SOLID 가 뭐냐
5가지 객체지향 설계 원칙 의 머리글자. Robert C. Martin (Uncle Bob) 이 정리. 면접에서 반드시 한 번은 묻습니다.
S — Single Responsibility (단일 책임)
한 클래스는 한 가지 일만. 변경 사유가 1개여야 함.
O — Open/Closed (개방/폐쇄)
확장엔 열려있고 수정엔 닫혀있게. 새 기능 추가 시 기존 코드 수정 없이 가능해야.
L — Liskov Substitution (리스코프 치환)
자식이 부모를 완전히 대체 가능해야. 부모를 쓰는 코드에 자식을 넣어도 문제없이 동작.
상속 대신 인터페이스 분리 가 정답이 많습니다.
I — Interface Segregation (인터페이스 분리)
큰 인터페이스를 잘게 쪼개라. 안 쓰는 메서드까지 구현 강제 X.
D — Dependency Inversion (의존성 역전)
구체 클래스가 아니라 인터페이스에 의존. Spring 의 DI 가 이 원칙의 실천판.
외울 필요 없습니다 — 원칙은 결과 입니다
좋은 코드를 짜다 보면 자연스럽게 따라가는 원리 입니다. 이름을 외우려 하지 말고 각 원칙의 의도 만 기억하세요.
☕ 직접 실행 — class · 상속 · 다형성
🤖 AI 에게 이렇게 요청해보세요
이 lesson 의 개념을 알면 AI 에게 구체적으로 지시할 수 있습니다. 막연한 "고쳐줘" 가 아니라 어휘를 가진 요청 — 그게 토큰 절약의 출발점입니다.
- ▸"이 클래스를 SOLID 원칙 (특히 SRP) 위반 여부 검사해서 분리해줘"
- ▸"이 객체 생성 코드에 빌더 패턴 적용해줘"
- ▸"이 상속 구조를 인터페이스 분리로 리팩토링해줘"
왜 이게 토큰을 줄이나
개념을 모를 땐 AI 답변을 받고도 "그게 뭐예요?" 를 다시 물어야 합니다. 그 "다시 물음" 이 토큰을 잡아먹습니다. 개념 한 번 익혀두면 대화가 한 번에 끝납니다.