Understand it step by step (한국어로 이해 → 영어로 말하기)
- 1
먼저 요구사항을 정리하고 핵심 개념을 enum으로 잡습니다. 이동 방향(UP/DOWN/IDLE)과 문 상태(OPEN/CLOSED)는 값이 정해져 있으니 클래스가 아니라 enum이 맞습니다.
⚖️ Trade-off: 방향이나 문 상태를 String이나 int로 표현할 수도 있습니다. 하지만 그러면 잘못된 값('UPP', 99)이 들어갈 수 있습니다.
✅ Fix: 정해진 값 집합은 enum으로 만들어 컴파일 시점에 안전하게 막습니다.
🔧 도구:enum over primitivestype safety
enum Direction { UP, DOWN, IDLE } enum DoorState { OPEN, CLOSED }🗣 영어로 말해“Fixed value sets like direction and door state should be enums, not strings.”
checking microphone…
- 2
사용자의 호출을 표현하는 Request 클래스를 만듭니다. 어느 층에서 어느 방향으로 가고 싶은지를 담습니다. 한 번 만들면 바뀌지 않으니 필드를 final로 둡니다.
✅ Fix: Request는 불변 객체로 두어 여러 엘리베이터 사이에서 안전하게 넘길 수 있게 합니다.
🔧 도구:immutabilityvalue object
static final class Request { final int floor; final Direction direction; Request(int floor, Direction direction) { this.floor = floor; this.direction = direction; } }🗣 영어로 말해“A Request is an immutable value object holding the source floor and direction.”
checking microphone…
- 3
핵심 엔티티인 Elevator 클래스를 만듭니다. 현재 층, 이동 방향, 문 상태를 가집니다. 가야 할 층들은 위로 가는 정류장과 아래로 가는 정류장으로 나눠 TreeSet에 저장합니다.
⚖️ Trade-off: 정류장을 그냥 List에 담을 수도 있지만, 그러면 다음에 어느 층에 멈출지 매번 정렬하거나 찾아야 합니다.
✅ Fix: TreeSet을 쓰면 자동 정렬됩니다. 위는 오름차순, 아래는 내림차순(reverseOrder)으로 두면 first()가 항상 다음 목적지입니다.
🔧 도구:SRPTreeSet for ordered stops
static final class Elevator { final int id; int currentFloor = 0; Direction direction = Direction.IDLE; DoorState door = DoorState.CLOSED; final TreeSet<Integer> upStops = new TreeSet<>(); final TreeSet<Integer> downStops = new TreeSet<>(Comparator.reverseOrder()); Elevator(int id) { this.id = id; } }🗣 영어로 말해“Each elevator tracks its floor and keeps up and down stops in sorted sets.”
checking microphone…
- 4
엘리베이터의 핵심 동작을 추가합니다. addStop은 요청 층이 현재 층보다 위면 upStops, 아래면 downStops에 넣고, 현재 층이면 바로 문을 엽니다(놓치면 안 됨). step은 한 틱마다 한 층씩 목적지로 이동합니다.
✅ Fix: 방향에 따라 active 집합(upStops 또는 downStops)을 고르고, first()로 다음 목적지를 정해 한 층씩 움직입니다. 도착하면 그 정류장을 지우고 문을 엽니다.
🔧 도구:SCAN/elevator algorithmbehavior near data
void addStop(int floor) { if (floor > currentFloor) upStops.add(floor); else if (floor < currentFloor) downStops.add(floor); else door = DoorState.OPEN; // request for the current floor: open now } void step() { if (direction == Direction.IDLE) { if (!upStops.isEmpty()) direction = Direction.UP; else if (!downStops.isEmpty()) direction = Direction.DOWN; else return; } TreeSet<Integer> active = direction == Direction.UP ? upStops : downStops; if (active.isEmpty()) { direction = Direction.IDLE; return; } int target = active.first(); currentFloor += Integer.signum(target - currentFloor); if (currentFloor == target) { active.remove(target); door = DoorState.OPEN; } else door = DoorState.CLOSED; }🗣 영어로 말해“Each step moves the car one floor toward the next stop and opens the door on arrival.”
checking microphone…
- 5
어느 엘리베이터를 보낼지 고르는 정책을 인터페이스로 분리합니다. DispatchStrategy를 만들고, 가장 가까운 엘리베이터를 고르는 NearestCarStrategy를 구현합니다.
⚖️ Trade-off: 선택 로직을 컨트롤러 안에 if문으로 박을 수도 있습니다. 하지만 나중에 '가장 적게 멈추는 차' 같은 다른 정책을 원하면 컨트롤러를 고쳐야 합니다.
✅ Fix: Strategy 패턴으로 정책을 인터페이스 뒤에 두면, 컨트롤러를 안 건드리고 새 정책을 끼워 넣을 수 있습니다 (OCP).
🔧 도구:Strategy patternOCPDIP
interface DispatchStrategy { Elevator selectElevator(List<Elevator> elevators, Request request); } static final class NearestCarStrategy implements DispatchStrategy { public Elevator selectElevator(List<Elevator> elevators, Request request) { Elevator best = null; int bestDist = Integer.MAX_VALUE; for (Elevator e : elevators) { int d = Math.abs(e.currentFloor - request.floor); if (d < bestDist) { bestDist = d; best = e; } } return best; } }🗣 영어로 말해“Dispatch logic lives behind a Strategy interface so we can swap selection rules freely.”
checking microphone…
- 6
전체를 묶는 ElevatorController를 만듭니다. 엘리베이터 목록과 정책을 들고 있습니다. requestElevator는 정책으로 차를 고르고 그 차에 정류장을 추가하며, tick은 모든 차를 한 틱 움직입니다.
⚖️ Trade-off: 여러 사용자가 동시에 버튼을 누르면 같은 엘리베이터 상태를 동시에 건드려 데이터가 깨질 수 있습니다.
✅ Fix: requestElevator를 synchronized로 두어 한 번에 한 요청만 차를 배정하게 합니다. 더 크면 차별로 락을 나누거나 요청 큐를 쓸 수 있습니다.
🔧 도구:Facade/Controllersynchronizedthread safety
static final class ElevatorController { final List<Elevator> elevators = new ArrayList<>(); final DispatchStrategy strategy; ElevatorController(int count, DispatchStrategy strategy) { this.strategy = strategy; for (int i = 0; i < count; i++) elevators.add(new Elevator(i)); } synchronized void requestElevator(Request request) { Elevator e = strategy.selectElevator(elevators, request); if (e != null) e.addStop(request.floor); } void tick() { for (Elevator e : elevators) e.step(); } }🗣 영어로 말해“The controller picks a car via the strategy and synchronizes concurrent requests.”
checking microphone…
- 7
마지막으로 확장 지점을 설명하고 끝을 맺습니다. 현재 구조는 새 정책(예: 부하 분산), 우선순위 요청(소방관 모드), 엘리베이터 상태 머신(고장/점검) 등을 쉽게 붙일 수 있습니다.
✅ Fix: Direction/DoorState를 정식 State 패턴으로 키우거나, 도착 알림을 Observer로 표시 패널에 보내는 식으로 확장할 수 있습니다.
🔧 도구:State pattern (future)Observer (future)extensibility
// 확장 예: 우선순위 모드, 부하 분산 정책, 상태 머신 // new ElevatorController(4, new LeastBusyStrategy()); ElevatorController c = new ElevatorController(2, new NearestCarStrategy()); c.requestElevator(new Request(5, Direction.UP)); for (int i = 0; i < 6; i++) c.tick();
🗣 영어로 말해“This design extends cleanly to new strategies, priority modes, and state machines.”
checking microphone…
7단계 영어를 다 말하면 → 이 메커니즘 전체를 영어로 설명할 수 있게 된다.
Full Java (javac-verified)
import java.util.*;
public class Solution {
enum Direction { UP, DOWN, IDLE }
enum DoorState { OPEN, CLOSED }
interface DispatchStrategy {
Elevator selectElevator(List<Elevator> elevators, Request request);
}
static final class Request {
final int floor;
final Direction direction;
Request(int floor, Direction direction) {
this.floor = floor;
this.direction = direction;
}
}
static final class Elevator {
final int id;
int currentFloor = 0;
Direction direction = Direction.IDLE;
DoorState door = DoorState.CLOSED;
final TreeSet<Integer> upStops = new TreeSet<>();
final TreeSet<Integer> downStops = new TreeSet<>(Comparator.reverseOrder());
Elevator(int id) { this.id = id; }
void addStop(int floor) {
if (floor > currentFloor) upStops.add(floor);
else if (floor < currentFloor) downStops.add(floor);
}
void step() {
if (direction == Direction.IDLE) {
if (!upStops.isEmpty()) direction = Direction.UP;
else if (!downStops.isEmpty()) direction = Direction.DOWN;
else return;
}
TreeSet<Integer> active = direction == Direction.UP ? upStops : downStops;
if (active.isEmpty()) { direction = Direction.IDLE; return; }
int target = active.first();
currentFloor += Integer.signum(target - currentFloor);
if (currentFloor == target) {
active.remove(target);
door = DoorState.OPEN;
} else {
door = DoorState.CLOSED;
}
}
}
static final class NearestCarStrategy implements DispatchStrategy {
public Elevator selectElevator(List<Elevator> elevators, Request request) {
Elevator best = null;
int bestDist = Integer.MAX_VALUE;
for (Elevator e : elevators) {
int d = Math.abs(e.currentFloor - request.floor);
if (d < bestDist) { bestDist = d; best = e; }
}
return best;
}
}
static final class ElevatorController {
final List<Elevator> elevators = new ArrayList<>();
final DispatchStrategy strategy;
ElevatorController(int count, DispatchStrategy strategy) {
this.strategy = strategy;
for (int i = 0; i < count; i++) elevators.add(new Elevator(i));
}
synchronized void requestElevator(Request request) {
Elevator e = strategy.selectElevator(elevators, request);
if (e != null) e.addStop(request.floor);
}
void tick() { for (Elevator e : elevators) e.step(); }
}
public static void main(String[] args) {
ElevatorController c = new ElevatorController(2, new NearestCarStrategy());
c.requestElevator(new Request(5, Direction.UP));
for (int i = 0; i < 6; i++) c.tick();
System.out.println("Elevator 0 floor: " + c.elevators.get(0).currentFloor);
}
}