OOP 면접 질문
객체 지향 프로그래밍(OOP) 인터뷰는 캡슐화, 상속, 다형성, 추상화와 같은 기본 개념에 대한 이해와 디자인 패턴 적용 및 깔끔하고 유지보수 가능한 코드 작성 능력을 평가합니다. 이러한 질문은 주니어부터 시니어까지 모든 수준의 소프트웨어 엔지니어링 역할에 공통적이지만, 설계 및 아키텍처 결정이 중요한 시니어 직책에서 특히 중요합니다. 면접관은 개념을 명확히 설명하고 실제 예와 연결하며 OOP 원칙을 보여주는 코딩 문제를 해결할 것으로 기대합니다.
OOP 면접에서 다루는 내용
핵심 원칙
캡슐화, 상속, 다형성, 추상화에 대한 질문과 예제 및 실제 비유.
디자인 패턴
싱글톤, 팩토리, 옵저버, 전략과 같은 일반적인 패턴, 구현 및 사용 시기.
클래스 설계 및 관계
상속 대 합성, 추상 클래스 대 인터페이스, 결합/응집, SOLID 원칙.
실습 코딩 문제
클래스 계층 구현, 주차장 또는 도서관 시스템 설계, 절차적 코드를 OOP로 리팩토링.
샘플 OOP 면접 질문
- OOP의 네 가지 기둥을 설명하고 실제 예를 제시하세요.좋은 답변이 다루는 것
- 캡슐화(Encapsulation): 데이터와 메서드를 하나의 단위로 묶고 외부 접근을 제한합니다.
- 상속(Inheritance): 부모 클래스의 속성과 메서드를 자식 클래스가 재사용합니다.
- 다형성(Polymorphism): 같은 인터페이스나 메서드가 서로 다른 방식으로 동작합니다.
- 추상화(Abstraction): 복잡한 구현을 숨기고 필수적인 특징만 노출합니다.
샘플 답변 보기
OOP의 네 가지 기둥은 캡슐화, 상속, 다형성, 추상화입니다. 캡슐화는 예를 들어 은행 계좌 클래스에서 잔액을 private 필드로 두고 public 메서드로만 접근하게 하는 것입니다. 상속은 자동차 클래스를 기반으로 전기차 클래스가 extends를 통해 배터리 관련 기능을 추가하는 경우입니다. 다형성은 동물 클래스의 '울다' 메서드를 고양이와 개가 각각 다르게 구현하는 것입니다. 추상화는 데이터베이스 연결을 인터페이스로 정의하고 다양한 DB 드라이버가 구현하는 것입니다. 이 네 가지 원칙은 코드 재사용성, 유지보수성, 확장성을 높이는 핵심입니다.
- 추상 클래스와 인터페이스의 차이점은 무엇인가요? 각각 언제 사용하나요?좋은 답변이 다루는 것
- 추상 클래스는 abstract 키워드로 선언하며 인스턴스화 불가, 하나만 상속 가능.
- 인터페이스는 interface 키워드, 다중 구현 가능, Java 8부터 default 메서드 허용.
- 추상 클래스는 공통 상태(필드)와 부분 구현을 제공할 때 사용.
- 인터페이스는 완전한 추상화와 계약(behavior)을 정의할 때 사용.
- 추상 클래스는 'is-a' 관계, 인터페이스는 'can-do' 관계에 적합.
샘플 답변 보기
추상 클래스와 인터페이스의 주요 차이점은 상태 보유 여부, 다중 상속 가능성, 멤버 접근 제어자입니다. 추상 클래스는 필드, 생성자, 일반 메서드와 추상 메서드를 가질 수 있으며 단일 상속만 가능합니다. 반면 인터페이스는 Java 8 이후 default와 static 메서드를 가질 수 있지만 필드는 public static final 상수만 가능하며 다중 구현이 가능합니다. 추상 클래스는 관련 클래스 간의 공통된 상태와 행위를 공유할 때 사용하고, 인터페이스는 서로 관련 없는 클래스가 특정 기능을 구현하도록 강제할 때 사용합니다. 예를 들어 동물 계층에서는 추상 클래스 Animal을, 날 수 있는 능력은 Flyable 인터페이스로 정의하는 것이 좋습니다.
- Java(또는 선택한 언어)로 싱글톤 패턴을 구현하고 스레드 안전성에 대해 논의하세요.좋은 답변이 다루는 것
- 싱글톤 패턴은 클래스의 인스턴스를 하나만 생성하고 전역 접근을 제공합니다.
- 스레드 안전 구현: double-checked locking, static holder, enum 방법.
- enum 방식이 가장 간단하고 직렬화와 리플렉션에도 안전합니다.
- 클래스 로딩 시점에 인스턴스가 생성되어 lazy loading이 필요하면 static holder 사용.
샘플 답변 보기
싱글톤 패턴은 애플리케이션에서 하나의 인스턴스만 필요한 경우(예: 설정 관리자, 로거) 사용됩니다. 스레드 안전성을 고려하지 않으면 여러 스레드가 동시에 생성 메서드를 호출할 때 여러 인스턴스가 생길 수 있습니다. Java에서는 double-checked locking에 volatile 키워드를 사용하거나, static inner class를 이용한 holder 패턴, 또는 enum을 사용하는 것이 일반적입니다. enum 방식은 코드가 간결하고 직렬화와 리플렉션 공격에도 안전하여 가장 추천됩니다. double-checked locking은 성능과 안전성 사이의 균형을 제공하지만 구현이 복잡합니다. 아래 코드는 double-checked locking과 enum 두 가지 방식을 보여줍니다.
참고 코드java // double-checked locking 방식 public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } // enum 방식 (가장 안전하고 간단) public enum SingletonEnum { INSTANCE; public void doSomething() { // 메서드 구현 } } // 시간 복잡도: O(1), 공간 복잡도: O(1) (인스턴스 하나) // 스레드 안전: synchronized 블록 또는 JVM의 enum 초기화로 보장 - 도형(Circle, Rectangle)에 대한 클래스 계층을 면적 계산과 함께 설계하여 다형성을 보여주세요.좋은 답변이 다루는 것
- 도형 클래스 계층: 추상 클래스 Shape에 abstract 메서드 area() 정의.
- Circle과 Rectangle이 Shape를 상속받아 area()를 각각 구현.
- 다형성: Shape 타입 변수로 다양한 도형을 참조하고 area() 호출.
- 확장성: 새 도형(Triangle)을 추가하려면 Shape를 상속하기만 하면 됨.
샘플 답변 보기
도형 계층 설계에서 다형성을 보여주기 위해 추상 클래스 Shape를 만들고 area()를 추상 메서드로 선언합니다. Circle과 Rectangle 클래스가 Shape를 상속받아 각각의 면적 계산 로직을 구현합니다. 클라이언트 코드에서는 Shape 타입의 리스트를 만들어 다양한 도형 객체를 저장하고, 반복문으로 각 도형의 area()를 호출하면 실제 객체에 따라 다른 계산이 수행됩니다. 이는 다형성의 전형적인 예로, 새로운 도형이 추가되어도 기존 코드를 변경하지 않아도 됩니다. 아래 코드는 Java로 구현한 예시입니다.
참고 코드java // 추상 클래스 Shape abstract class Shape { public abstract double area(); } // Circle 클래스 class Circle extends Shape { private double radius; public Circle(double radius) { this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } } // Rectangle 클래스 class Rectangle extends Shape { private double width, height; public Rectangle(double width, double height) { this.width = width; this.height = height; } @Override public double area() { return width * height; } } // 다형성 사용 예 public class Main { public static void main(String[] args) { List<Shape> shapes = new ArrayList<>(); shapes.add(new Circle(5)); shapes.add(new Rectangle(4, 6)); for (Shape s : shapes) { System.out.println("Area: " + s.area()); } } } // 시간 복잡도: 각 area() 메서드는 O(1), 공간 복잡도: O(n) (n개의 도형 객체) - 상속이 어떻게 강한 결합을 초래하나요? 합성을 상속보다 선호하세요 – 예를 들어 설명하세요.좋은 답변이 다루는 것
- 상속은 부모 클래스의 구현에 자식 클래스가 의존하여 강한 결합을 만듭니다.
- 부모 클래스 변경 시 모든 자식 클래스에 영향이 갑니다.
- 합성은 인터페이스를 통해 느슨한 결합을 유지하고 런타임에 행위 변경 가능.
- 예: Flyable 인터페이스를 합성하여 Bird, Airplane에 날기 행위를 주입.
샘플 답변 보기
상속은 부모 클래스의 내부 구현에 자식 클래스가 직접 의존하기 때문에 강한 결합(tight coupling)을 초래합니다. 부모 클래스의 메서드 시그니처나 로직이 변경되면 모든 자식 클래스를 수정해야 할 수 있습니다. 또한 상속 계층이 깊어지면 복잡도가 증가하고, 부모 클래스의 불필요한 메서드가 자식에게 노출될 수 있습니다. 합성(composition)은 객체가 다른 객체를 포함하고 그 객체의 인터페이스를 통해 기능을 위임하는 방식입니다. 예를 들어, '날기' 행위를 Flyable 인터페이스로 정의하고, Bird 클래스는 Flyable 구현체를 필드로 가지며 fly() 메서드에서 위임합니다. 이렇게 하면 Flyable 구현을 교체하기 쉽고, Bird는 FlightBehavior에만 의존하므로 결합도가 낮아집니다. 합성은 상속보다 더 유연하고 변경에 강한 설계를 가능하게 합니다.
- 배송비를 계산하기 위해 전략 패턴을 구현하는 코드를 작성하세요.좋은 답변이 다루는 것
- 전략 패턴은 알고리즘군을 정의하고 각각 캡슐화하여 교체 가능하게 합니다.
- ShippingCostStrategy 인터페이스에 calculateCost(Order) 메서드 선언.
- StandardShipping, ExpressShipping, InternationalShipping 구체 전략 구현.
- Order 클래스는 ShippingCostStrategy를 합성하여 배송비 계산 위임.
샘플 답변 보기
전략 패턴을 사용하여 배송비 계산을 설계하면, 다양한 배송 방식에 따른 비용 계산 로직을 분리할 수 있습니다. 먼저 ShippingCostStrategy 인터페이스를 정의하고 calculateCost(Order) 메서드를 선언합니다. 그 후 StandardShipping, ExpressShipping, InternationalShipping 등 구체적인 전략 클래스에서 이 인터페이스를 구현합니다. Order 클래스는 ShippingCostStrategy 타입의 필드를 가지고 있으며, 생성자나 세터를 통해 전략을 주입받습니다. 배송비 계산이 필요할 때 order.shippingStrategy.calculateCost(this)를 호출합니다. 이렇게 하면 새로운 배송 방식이 추가되어도 기존 코드를 수정하지 않고 새로운 전략 클래스를 추가하기만 하면 됩니다. 아래 코드는 Java로 구현한 예시입니다.
참고 코드java // 전략 인터페이스 interface ShippingCostStrategy { double calculateCost(Order order); } // 구체 전략: 표준 배송 class StandardShipping implements ShippingCostStrategy { @Override public double calculateCost(Order order) { return order.getWeight() * 1.5; } } // 구체 전략: 익일 배송 class ExpressShipping implements ShippingCostStrategy { @Override public double calculateCost(Order order) { return order.getWeight() * 3.0 + 10; } } // 구체 전략: 국제 배송 class InternationalShipping implements ShippingCostStrategy { @Override public double calculateCost(Order order) { return order.getWeight() * 5.0 + 15; } } // 주문 클래스 (Context) class Order { private double weight; private ShippingCostStrategy shippingStrategy; public Order(double weight, ShippingCostStrategy shippingStrategy) { this.weight = weight; this.shippingStrategy = shippingStrategy; } public double getWeight() { return weight; } public double calculateShippingCost() { return shippingStrategy.calculateCost(this); } } // 사용 예 public class Main { public static void main(String[] args) { Order order = new Order(10, new ExpressShipping()); System.out.println("배송비: " + order.calculateShippingCost()); } } // 시간 복잡도: O(1), 공간 복잡도: O(1) - 리스코프 치환 원칙이란 무엇인가요? 위반하는 코드 예제를 제시하세요.좋은 답변이 다루는 것
- LSP: 자식 클래스는 부모 클래스를 대체해도 프로그램이 정상 동작해야 함.
- 위반 예: Rectangle과 Square, Square가 Rectangle을 상속받을 때.
- Square는 setWidth/setHeight가 불변식을 깨뜨림.
- 해결: 별도의 클래스로 만들거나 합성을 사용.
샘플 답변 보기
리스코프 치환 원칙(LSP)은 'S가 T의 하위 타입이라면, T 타입의 객체를 S 타입의 객체로 치환해도 프로그램의 속성이 변하지 않아야 한다'는 원칙입니다. 즉, 자식 클래스는 부모 클래스의 기능을 온전히 대체할 수 있어야 합니다. 가장 유명한 위반 사례는 Rectangle과 Square 관계입니다. Square가 Rectangle을 상속받으면 setWidth와 setHeight 메서드가 모순됩니다. 예를 들어, Rectangle 타입 변수가 setWidth(5) 후 setHeight(10)을 호출해도 너비 5, 높이 10이 유지되어야 하지만, Square는 너비와 높이가 같아야 하므로 setWidth(5) 후 setHeight(10)을 호출하면 너비도 10으로 변해 불변식을 깨트립니다. 따라서 Rectangle을 사용하는 코드에서 Square로 교체하면 오작동할 수 있습니다. 해결 방법은 Rectangle과 Square를 별도의 클래스로 만들거나, Shape 인터페이스를 도입하고 각각 구현하는 것입니다.
- 레스토랑 주문 시스템을 MenuItem, Order, Payment 클래스로 설계하고 좋은 OOP 설계를 보장하세요.좋은 답변이 다루는 것
- MenuItem: name, price, category 등 속성과 getter/setter.
- Order: orderId, List<OrderItem>, total 계산 메서드.
- OrderItem: MenuItem, quantity 필드로 구성.
- Payment: PaymentStrategy 인터페이스를 통해 다양한 결제 방식 지원.
- 좋은 OOP 설계: 각 클래스는 단일 책임, 느슨한 결합, 캡슐화.
샘플 답변 보기
레스토랑 주문 시스템을 설계할 때는 각 클래스가 단일 책임을 가지도록 해야 합니다. MenuItem은 메뉴 항목의 정보(이름, 가격, 카테고리)를 저장하고, Order는 주문 정보(주문 번호, 주문 항목 리스트, 총 금액 계산)를 담당합니다. OrderItem은 MenuItem과 수량을 포함하여 각 항목의 소계를 계산합니다. Payment는 결제 방식을 추상화하기 위해 PaymentStrategy 인터페이스를 사용하여 현금, 카드, 모바일 결제 등을 전략 패턴으로 구현합니다. 이렇게 하면 각 클래스의 변경이 다른 클래스에 미치는 영향을 최소화하고, 새로운 결제 방식이나 메뉴 카테고리 추가가 용이합니다. 아래 코드는 Java로 간단히 구현한 예시입니다.
참고 코드java import java.util.ArrayList; import java.util.List; // MenuItem 클래스 class MenuItem { private String name; private double price; private String category; public MenuItem(String name, double price, String category) { this.name = name; this.price = price; this.category = category; } // getter 생략 } // OrderItem 클래스 class OrderItem { private MenuItem menuItem; private int quantity; public OrderItem(MenuItem menuItem, int quantity) { this.menuItem = menuItem; this.quantity = quantity; } public double getSubtotal() { return menuItem.getPrice() * quantity; } } // Order 클래스 class Order { private int orderId; private List<OrderItem> items = new ArrayList<>(); public Order(int orderId) { this.orderId = orderId; } public void addItem(OrderItem item) { items.add(item); } public double getTotal() { return items.stream().mapToDouble(OrderItem::getSubtotal).sum(); } } // PaymentStrategy 인터페이스 interface PaymentStrategy { void pay(double amount); } // 구체 결제 전략 class CreditCardPayment implements PaymentStrategy { @Override public void pay(double amount) { System.out.println("신용카드로 " + amount + "원 결제"); } } class CashPayment implements PaymentStrategy { @Override public void pay(double amount) { System.out.println("현금으로 " + amount + "원 결제"); } } // Payment 클래스 (전략 패턴 사용) class Payment { private PaymentStrategy strategy; public Payment(PaymentStrategy strategy) { this.strategy = strategy; } public void processPayment(double amount) { strategy.pay(amount); } } // 사용 예 public class RestaurantSystem { public static void main(String[] args) { MenuItem pizza = new MenuItem("피자", 15000, "메인"); MenuItem coke = new MenuItem("콜라", 2000, "음료"); Order order = new Order(1); order.addItem(new OrderItem(pizza, 2)); order.addItem(new OrderItem(coke, 3)); double total = order.getTotal(); Payment payment = new Payment(new CreditCardPayment()); payment.processPayment(total); } }
준비 방법
- 각 OOP 원칙에 대한 정의와 예제를 마스터하세요. 비유를 사용하여 설명할 준비를 하세요.
- 화이트보드나 코드 편집기에서 시스템 설계를 연습하며 클래스 책임과 관계에 집중하세요.
- 일반적인 디자인 패턴을 복습하고 선택한 언어로 처음부터 구현할 수 있어야 합니다.
- 상속 대 합성, 강한 결합 대 느슨한 결합, 추상 클래스 대 인터페이스 사용 시기 등 장단점을 이해하세요.
- 실습 코딩에 대비: 작은 OOP 프로그램을 작성하고 설계 선택을 설명하는 데 익숙해지세요.
자주 묻는 질문
가장 흔한 OOP 인터뷰 질문은 무엇인가요?
네 가지 기둥 설명, 추상 클래스와 인터페이스의 차이, 클래스 계층 설계, 싱글톤과 같은 디자인 패턴 구현 등이 일반적입니다.
OOP 코딩 문제를 어떻게 준비해야 하나요?
주차장, 도서관, 전자상거래 카트와 같은 작은 시스템 설계를 연습하세요. 클래스 구조, 캡슐화, 인터페이스 사용에 집중하세요.
OOP 인터뷰를 위해 디자인 패턴을 알아야 하나요?
네, 특히 시니어 역할의 경우. 가장 일반적인 싱글톤, 팩토리, 전략, 옵저버, 데코레이터를 알아두세요.
상속과 합성의 차이점은 무엇인가요?
상속은 'is-a' 관계를 만들고 합성은 'has-a' 관계를 만듭니다. 유연성과 느슨한 결합을 위해 합성을 선호하세요.
OOP 개념을 명확히 설명하려면 어떻게 해야 하나요?
실제 비유를 사용하세요: 자동차를 객체(색상, 모델과 같은 속성, 시작, 정지와 같은 메서드)로 설명하거나 동물원 동물 계층으로 다형성을 설명하세요.
즉각적인 AI 피드백으로 OOP 질문 연습하기
이력서를 업로드하고 맞춤형 모의 면접을 받아 무엇을 개선해야 할지 정확히 확인하세요 — 무료로 시작하세요.