이번 문서의 목표: 이 문서를 다 읽으면 Java 상속 구조 위에 JDBC 조회, 웹 요청 처리, 소켓 통신이 함께 얽힌 시나리오형 코드를 읽고, 빈칸을 채우거나 오류를 찾아 고칠 수 있다.
10~12편에서 Java 객체지향과 예외·컬렉션을, 18편에서 JDBC를, 20편에서 JSP·Servlet을, 21편에서 소켓을 각각 다뤘다. 독학사 4단계 시험의 마지막 퍼즐은 이 조각들을 하나의 이야기로 엮는 것이다 — “회원 정보를 담은 클래스 계층이 있고, DB에서 조회해서, 웹 요청으로 응답하거나 소켓으로 다른 서버에 전달한다”는 식의 시나리오다. 이 편은 그런 통합 시나리오 문제 세 개를 실제로 풀어본다.
시나리오 1 — 상속 구조와 다형성이 있는 회원 클래스
다음은 일반 회원과 우수 회원을 상속으로 표현한 클래스다. 이 시나리오에서는 상속·오버라이딩이 실제로 어떻게 동작하는지 먼저 확인한다.
class Member {
protected String name;
protected int point;
public Member(String name, int point) {
this.name = name;
this.point = point;
}
public double getDiscountRate() {
return 0.0; // 기본 회원은 할인 없음
}
public void printInfo() {
System.out.println(name + "님의 할인율: " + (getDiscountRate() * 100) + "%");
}
}
class VipMember extends Member {
public VipMember(String name, int point) {
super(name, point);
}
@Override
public double getDiscountRate() {
return point >= 1000 ? 0.1 : 0.05;
}
}
public class MemberTest {
public static void main(String[] args) {
Member[] members = {
new Member("일반회원", 200),
new VipMember("우수회원A", 500),
new VipMember("우수회원B", 1200)
};
for (Member m : members) {
m.printInfo();
}
}
}여기서 시험이 반드시 묻는 지점은 printInfo()가 Member에만 정의되어 있는데, 왜 VipMember 객체에서 호출해도 VipMember의 getDiscountRate()가 실행되는가이다. 답은 동적 바인딩(dynamic binding, 실행 시점에 실제 객체 타입을 보고 어떤 메서드를 실행할지 결정하는 방식)이다. Member[] members 배열은 정적 타입(컴파일 시점에 정해지는 타입, 여기서는 Member)으로 선언되어 있지만, 각 원소의 실제 타입(런타임에 생성된 객체의 진짜 타입)은 Member 또는 VipMember다. m.printInfo() 안에서 다시 getDiscountRate()를 호출할 때, 자바는 m의 정적 타입이 아니라 실제 타입에 오버라이딩된 메서드를 찾아 실행한다.
| 배열 원소 | 실제 타입 | point | getDiscountRate() 결과 | 출력 |
|---|---|---|---|---|
| members[0] | Member | 200 | 0.0(재정의 없음) | 일반회원님의 할인율: 0.0% |
| members[1] | VipMember | 500 | 0.05(500 < 1000) | 우수회원A님의 할인율: 5.0% |
| members[2] | VipMember | 1200 | 0.1(1200 >= 1000) | 우수회원B님의 할인율: 10.0% |
일반회원님의 할인율: 0.0%
우수회원A님의 할인율: 5.0%
우수회원B님의 할인율: 10.0%시나리오 2 — JDBC로 회원을 조회해 예외 처리까지 연결하기
이제 위 회원 정보를 DB에서 읽어오는 코드를 붙인다. member 테이블은 name, point 컬럼을 가진다고 가정한다.
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class MemberDao {
public List<Member> findAll() {
List<Member> result = new ArrayList<>();
String sql = "SELECT name, point FROM member";
try (Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/shop", "user", "pass");
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
String name = rs.getString("name");
int point = rs.getInt("point");
Member m = (point >= 500) ? new VipMember(name, point) : new Member(name, point);
result.add(m);
}
} catch (SQLException e) {
System.out.println("DB 조회 오류: " + e.getMessage());
}
return result;
}
}이 코드에서 눈여겨볼 지점 세 가지가 있다.
- try-with-resources:
try (...)괄호 안에 선언한Connection,PreparedStatement,ResultSet은try블록이 끝나면(정상 종료든 예외든) 자바가 자동으로close()를 호출해 자원을 반납한다. 18편에서 배운 “자원 반납을finally에서 수동으로 하던 방식”을 더 안전하게 대체한 문법이다. ResultSet.next()의 역할: 커서를 다음 행으로 이동시키고, 이동할 행이 있으면true, 더 이상 없으면false를 반환한다. 그래서while (rs.next())가 “행이 남아 있는 동안 반복”이라는 의미가 된다.- DB 조회 결과로 다형성 객체 생성:
point값에 따라Member또는VipMember를 생성해 같은 리스트(List<Member>)에 담는다. 리스트의 선언 타입은Member이지만 실제로는 두 종류의 객체가 섞여 있으며, 시나리오 1에서 본 동적 바인딩이 여기서도 그대로 적용된다.
SQLException을 잡지 않고 무시하면 어떤 문제가 생기는지도 시험 포인트다. SQLException은 검사 예외(checked exception, 컴파일러가 처리(try-catch 또는 throws)를 강제하는 예외)이므로, 처리하지 않으면 애초에 컴파일이 되지 않는다. 위 코드처럼 catch 블록에서 오류 메시지만 출력하고 빈 리스트를 반환하게 두면, 호출한 쪽은 “회원이 원래 없는 것”과 “DB 연결에 실패한 것”을 구분하지 못하는 문제가 생긴다는 점도 실전에서는 함께 언급된다.
시나리오 3 — 웹 요청과 소켓 통신을 잇는 처리 흐름
마지막으로 위 MemberDao를 웹 계층에서 호출하는 흐름을 정리한다. 실제 서블릿 컨테이너 없이도 흐름을 코드로 이해할 수 있도록, Servlet의 핵심 메서드 구조만 옮긴다.
public class MemberListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
MemberDao dao = new MemberDao();
List<Member> members = dao.findAll();
request.setAttribute("members", members);
request.getRequestDispatcher("/memberList.jsp").forward(request, response);
}
}이 코드가 실행되는 순서는 다음과 같다.
doGet이 자동으로 호출되는 이유는 20편에서 배운 서블릿 생명주기(life cycle) 때문이다. 서블릿 컨테이너는 HTTP 메서드가 GET이면 doGet을, POST면 doPost를 호출하도록 정해 놓았다 — 개발자가 직접 이 메서드들을 호출할 필요가 없다. request.setAttribute()로 담은 members 객체는 forward()로 넘어간 JSP에서 ${members}(EL, Expression Language) 형태로 꺼내 화면에 뿌린다.
이 회원 목록을 다른 지점 서버로 실시간 전달해야 한다면, 21편의 소켓을 이용해 다음과 같이 간단히 확장할 수 있다.
public void notifyBranchServer(List<Member> members) {
try (Socket socket = new Socket("branch-server.local", 6000);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
for (Member m : members) {
out.println(m.name + "," + m.point); // 회원 한 명씩 한 줄로 전송
}
} catch (IOException e) {
System.out.println("지점 서버 통신 오류: " + e.getMessage());
}
}이 메서드가 던질 수 있는 예외는 IOException이며, 21편에서 본 것처럼 new Socket(host, port) 시점에 연결이 실패하면(서버가 꺼져 있거나 포트가 닫혀 있으면) 이 예외가 발생한다. try-with-resources로 Socket과 PrintWriter를 감쌌으므로, 예외가 나든 정상 종료되든 close()가 보장된다.
시나리오 통합: 코드 빈칸 채우기 연습
다음은 위 세 시나리오를 압축한 코드다. 빈칸 (가), (나)에 들어갈 내용을 생각해 보자.
public double getDiscountRate() {
return point >= 1000 ? 0.1 : 0.05;
}위는 VipMember의 실제 구현이다. 이제 다음 코드의 (가)에 들어가 새 회원 객체를 만드는 조건을 완성해야 한다고 하자.
Member m = (point >= 500) ? (가) : new Member(name, point);(가)에는 new VipMember(name, point)가 들어가야 한다. 이는 시나리오 2에서 “point가 500 이상이면 VipMember로 만든다”는 규칙을 그대로 코드로 옮긴 것이며, Member형 변수 m에 VipMember 객체를 대입하는 것은 업캐스팅(upcasting, 자식 타입 객체를 부모 타입 참조 변수에 대입하는 것으로 항상 안전하게 허용된다)이라 문법적으로도 문제가 없다.
자주 틀리는 점
Member[] members배열 선언 타입만 보고 모든 원소가getDiscountRate()에서 항상0.0을 반환한다고 착각한다. 실제 타입이VipMember인 원소는 오버라이딩된 메서드가 실행된다.try-with-resources를 쓰면catch블록이 필요 없다고 착각한다. 자원 반납(close())이 자동화될 뿐, 예외 자체를 처리하려면 여전히catch가 필요하다.- 서블릿의
doGet을 개발자가 직접 호출해야 한다고 착각한다. 서블릿 컨테이너가 HTTP 메서드에 따라 자동으로 호출한다. - 소켓 통신 코드에서
IOException을 잡지 않아도 된다고 착각한다.IOException은 검사 예외이므로try-catch또는throws선언이 없으면 컴파일이 되지 않는다.
핵심 정리
- 배열이나 컬렉션의 선언 타입이 부모 클래스여도, 실제 저장된 객체의 타입에 따라 오버라이딩된 메서드가 실행되는 것이 동적 바인딩이다.
- JDBC의
try-with-resources는Connection·PreparedStatement·ResultSet을 자동으로 닫아 주지만,SQLException같은 검사 예외 처리는 별도로 필요하다. - 서블릿은 HTTP 메서드에 맞춰 컨테이너가
doGet·doPost를 자동 호출하며,request.setAttribute()와forward()로 JSP에 데이터를 넘긴다. - 소켓 통신은
IOException을 던질 수 있는 검사 예외 상황이며,try-with-resources로 자원 반납을 자동화할 수 있다. - 여러 계층(객체지향·DB·웹·네트워크)이 섞인 시나리오 문제는 “이 객체의 실제 타입이 무엇인가”, “이 예외는 검사 예외인가”라는 두 질문으로 대부분 정리된다.