Skip to Content
독학사독학사 4단계통합프로그래밍22. 통합 실습 ②: Java·웹·DB·네트워크 연계 손코딩

이번 문서의 목표: 이 문서를 다 읽으면 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 객체에서 호출해도 VipMembergetDiscountRate()가 실행되는가이다. 답은 동적 바인딩(dynamic binding, 실행 시점에 실제 객체 타입을 보고 어떤 메서드를 실행할지 결정하는 방식)이다. Member[] members 배열은 정적 타입(컴파일 시점에 정해지는 타입, 여기서는 Member)으로 선언되어 있지만, 각 원소의 실제 타입(런타임에 생성된 객체의 진짜 타입)은 Member 또는 VipMember다. m.printInfo() 안에서 다시 getDiscountRate()를 호출할 때, 자바는 m의 정적 타입이 아니라 실제 타입에 오버라이딩된 메서드를 찾아 실행한다.

배열 원소실제 타입pointgetDiscountRate() 결과출력
members[0]Member2000.0(재정의 없음)일반회원님의 할인율: 0.0%
members[1]VipMember5000.05(500 < 1000)우수회원A님의 할인율: 5.0%
members[2]VipMember12000.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, ResultSettry 블록이 끝나면(정상 종료든 예외든) 자바가 자동으로 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-resourcesSocketPrintWriter를 감쌌으므로, 예외가 나든 정상 종료되든 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형 변수 mVipMember 객체를 대입하는 것은 업캐스팅(upcasting, 자식 타입 객체를 부모 타입 참조 변수에 대입하는 것으로 항상 안전하게 허용된다)이라 문법적으로도 문제가 없다.

자주 틀리는 점

  • Member[] members 배열 선언 타입만 보고 모든 원소가 getDiscountRate()에서 항상 0.0을 반환한다고 착각한다. 실제 타입이 VipMember인 원소는 오버라이딩된 메서드가 실행된다.
  • try-with-resources를 쓰면 catch 블록이 필요 없다고 착각한다. 자원 반납(close())이 자동화될 뿐, 예외 자체를 처리하려면 여전히 catch가 필요하다.
  • 서블릿의 doGet을 개발자가 직접 호출해야 한다고 착각한다. 서블릿 컨테이너가 HTTP 메서드에 따라 자동으로 호출한다.
  • 소켓 통신 코드에서 IOException을 잡지 않아도 된다고 착각한다. IOException은 검사 예외이므로 try-catch 또는 throws 선언이 없으면 컴파일이 되지 않는다.

핵심 정리

  • 배열이나 컬렉션의 선언 타입이 부모 클래스여도, 실제 저장된 객체의 타입에 따라 오버라이딩된 메서드가 실행되는 것이 동적 바인딩이다.
  • JDBC의 try-with-resourcesConnection·PreparedStatement·ResultSet을 자동으로 닫아 주지만, SQLException 같은 검사 예외 처리는 별도로 필요하다.
  • 서블릿은 HTTP 메서드에 맞춰 컨테이너가 doGet·doPost를 자동 호출하며, request.setAttribute()forward()로 JSP에 데이터를 넘긴다.
  • 소켓 통신은 IOException을 던질 수 있는 검사 예외 상황이며, try-with-resources로 자원 반납을 자동화할 수 있다.
  • 여러 계층(객체지향·DB·웹·네트워크)이 섞인 시나리오 문제는 “이 객체의 실제 타입이 무엇인가”, “이 예외는 검사 예외인가”라는 두 질문으로 대부분 정리된다.

마무리 복습

문제 14지선다
시나리오 1의 MemberTest 실행 결과에서 우수회원A(point=500)의 할인율 출력으로 옳은 것은?
문제 24지선다
Member[] members 배열에 VipMember 객체를 담아 m.printInfo()를 호출했을 때 VipMember의 getDiscountRate()가 실행되는 이유로 옳은 것은?
문제 34지선다
시나리오 2의 MemberDao.findAll()에서 try-with-resources 문법에 대한 설명으로 옳은 것은?
문제 44지선다
ResultSet의 next() 메서드에 대한 설명으로 옳은 것은?
문제 54지선다
MemberListServlet의 doGet() 메서드가 브라우저의 GET 요청 시 자동으로 호출되는 근거로 옳은 것은?
문제 64지선다
notifyBranchServer 메서드에서 new Socket(host, port) 호출 시 서버가 꺼져 있을 때 발생할 수 있는 예외로 옳은 것은?
문제 74지선다
point가 500 이상이면 new VipMember(name, point)를, 그렇지 않으면 new Member(name, point)를 대입해 Member m을 만드는 삼항 연산자 문장에서, VipMember 객체를 Member 타입 변수에 대입하는 것에 대한 설명으로 옳은 것은?

참고 자료

Last updated on