애플리케이션 아키텍처 패턴
데스크톱 소프트웨어는 크게 두 가지 구조로 분류됩니다.
- B/S 구조: 브라우저가 서버와 통신하는 방식
- C/S 구조: 전용 클라이언트 프로그램이 서버와 통신하는 방식
Tomcat 서버 디렉토리 구조
| 경로 | 용도 |
|---|---|
| /bin | 시작 및 종료 스크립트 |
| /conf | 설정 파일 |
| /lib | 필요한 JAR 라이브러리 |
| /logs | 로그 파일 |
| /temp | 임시 파일 |
| /webapps | 배포된 웹 애플리케이션 |
| /work | JSP에서 변환된 서블릿 |
포트 설정 변경
conf/server.xml에서 Connector 설정을 수정합니다.
<Connector port="9090" protocol="HTTP/1.1"
connectionTimeout="30000"
redirectPort="8443" />JSP 동작 원리
JSP는 서버에서 Java 소스로 변환되고, 클래스 파일로 컴파일되어 실행됩니다.
JSP 스크립트 요소
<% %>- Java 코드 블록<%= %>- 표현식 출력<% out.write("내용") %>- 출력 객체 사용<%@ page import="java.util.ArrayList" %>- 패키지 임포트
HTTP 메서드 비교
| 특성 | GET | POST |
|---|---|---|
| 캐싱 | 가능 | 불가 |
| 북마크 | 가능 | 불가 |
| 데이터 크기 | URL 길이 제한 | 제한 없음 |
| 인코딩 | 단일 형식 | 다중 형식 지원 |
| 문자 제한 | ASCII만 | 모든 문자, 바이너리 |
| 보안 | URL에 노출 | 요청 본문에 숨김 |
내장 객체 활용
출력 객체
out.write("화면에 표시될 내용");요청 객체
HTML 폼:
<input type="text" name="nickname"/>단일 값 수신:
String nickname = request.getParameter("nickname");다중 값 수신:
String[] interests = request.getParameterValues("interests");인코딩 문제 해결
POST 방식:
request.setCharacterEncoding("UTF-8");GET 방식 - 수동 변환:
nickname = new String(nickname.getBytes("ISO-8859-1"), "UTF-8");GET 방식 - 서버 설정:
<Connector port="9090"
protocol="HTTP/1.1"
connectionTimeout="30000"
redirectPort="8443"
URIEncoding="UTF-8"
useBodyEncodingForURI="true"/>응답 객체
페이지 이:
response.sendRedirect("/main.jsp");요청 전달 방식
포워딩
request.getRequestDispatcher("/target.jsp").forward(request, response);리다이렉트
response.sendRedirect("/target.jsp");차이점
- URL 변경: 포워딩은 유지, 리다이렉트는 변경
- 요청 횟수: 포워딩은 1회, 리다이렉트는 2회
- 데이터 전달: 포워딩은 가능, 리다이렉트는 불가
- 이동 범위: 포워딩은 내부만, 리다이렉트는 외부도 가능
시작 페이지 설정
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>자주 발생하는 오류
| 코드 | 원인 |
|---|---|
| 404 | 경로 오류, WEB-INF 위치, 미배포 |
| 500 | JSP 코드 오류 |
| 연결 불가 | 서버 미실행 |
에러 페이지 지정
<error-page>
<error-code>404</error-code>
<location>/error/notfound.html</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/error/servererror.html</location>
</error-page>JSP 내장 객체 9가지
| 객체 | 타입 | 범위 |
|---|---|---|
| request | HttpServletRequest | 요청 단위 |
| response | HttpServletResponse | 페이지 단위 |
| session | HttpSession | 사용자 세션 |
| application | ServletContext | 애플리케이션 전체 |
| out | JspWriter | 출력 버퍼 관리 |
| pageContext | PageContext | 모든 객체 접근 |
| config | ServletConfig | 설정 정보 |
| page | Object | 현재 페이지 |
| exception | Throwable | 에러 페이지 |
영역 객체 4단계
페이지 영역
pageContext.setAttribute("key", "값");
String val = (String) pageContext.getAttribute("key");
// 서블릿에서
PageContext ctx = JspFactory.getDefaultFactory().getPageContext(
this, request, response, null, true, 8192, true);요청 영역
request.setAttribute("user", member);
Member m = (Member) request.getAttribute("user");세션 영역
session.setAttribute("loginUser", member);
out.print(session.getId());
Member m = (Member) session.getAttribute("loginUser");
session.removeAttribute("loginUser");
session.setMaxInactiveInterval(120);
session.invalidate();
// 서블릿에서
HttpSession s = request.getSession();애플리케이션 영역
application.setAttribute("config", settings);
Settings s = (Settings) application.getAttribute("config");
// 서블릿에서
ServletContext ctx = this.getServletContext();쿠키 활용
// 저장
String id = URLEncoder.encode(userId, "UTF-8");
Cookie c = new Cookie("uid", id);
c.setPath("/");
c.setMaxAge(7200);
response.addCookie(c);
// 조회
Cookie[] arr = request.getCookies();
if (arr != null) {
for (Cookie c : arr) {
if ("uid".equals(c.getName())) {
String v = URLDecoder.decode(c.getValue(), "UTF-8");
}
}
}데이터베이스 연결
필요 라이브러리: mysql-connector-java-8.0.20.jar
기본 연결 흐름
- 드라이버 로드
- Connection 획득
- Statement 생성
- SQL 실행
- 자원 해제
Connection conn = null;
Statement stmt = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/mall?serverTimezone=Asia/Seoul";
conn = DriverManager.getConnection(url, "admin", "secret");
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT member_no, nickname FROM members");
while (rs.next()) {
long no = rs.getLong(1);
String name = rs.getString(2);
System.out.println(no + ": " + name);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try { stmt.close(); conn.close(); }
catch (SQLException e) { e.printStackTrace(); }
}주요 드라이버 클래스
- SQL Server: com.microsoft.sqlserver.jdbc.SQLServerDriver
- MySQL: com.mysql.cj.jdbc.Driver
- Oracle: oracle.jdbc.OracleDriver
Statement 메서드
| 메서드 | 반환 | 용도 |
|---|---|---|
| executeQuery | ResultSet | SELECT |
| executeUpdate | int | INSERT, UPDATE, DELETE |
| execute | boolean | 모든 SQL |
URL 파라미터
?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Seoul&zeroDateTimeBehavior=CONVERT_TO_NULLPreparedStatement 사용
public Member authenticate(String email, String password) {
Connection conn = null;
PreparedStatement pstmt = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mall", "admin", "secret");
String sql = "SELECT member_no, email FROM members WHERE email=? AND passwd=?";
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, email);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
Member m = null;
while (rs.next()) {
m = new Member();
m.setNo(rs.getLong("member_no"));
m.setEmail(rs.getString("email"));
}
return m;
} catch (Exception e) {
e.printStackTrace();
} finally {
try { pstmt.close(); conn.close(); }
catch (SQLException e) { e.printStackTrace(); }
}
return null;
}목록 조회
List<Product> list = new ArrayList<>();
while (rs.next()) {
Product p = new Product();
p.setCode(rs.getLong("product_code"));
p.setTitle(rs.getString("title"));
p.setDesc(rs.getString("description"));
list.add(p);
}
return list;정보 수정
String sql = "UPDATE products SET title=?, view_count=?, status=? WHERE code=?";
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, product.getTitle());
pstmt.setInt(2, product.getViewCount());
pstmt.setInt(3, product.getStatus());
pstmt.setLong(4, product.getCode());
return pstmt.executeUpdate();DAO 패턴 구조
- com.store.dao - 인터페이스 (MemberDao)
- com.store.dao.impl - 구현체 (MemberDaoImpl)
- com.store.entity - 도메인 객체 (Member)
- com.store.util - 공통 클래스 (DbUtil)
공통 유틸리티 클래스
public class DbUtil {
private Connection conn = null;
private PreparedStatement pstmt = null;
public boolean open() {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mall", "admin", "secret");
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public int modify(String sql, Object[] params) {
int result = 0;
try {
if (open()) {
pstmt = conn.prepareStatement(sql);
if (params != null) {
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
}
result = pstmt.executeUpdate();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
close();
}
return result;
}
public ResultSet query(String sql, Object[] params) {
try {
if (open()) {
pstmt = conn.prepareStatement(sql);
if (params != null) {
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
}
return pstmt.executeQuery();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public void close() {
try {
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}설정 파일 외부화
database.properties:
db.driver=com.mysql.cj.jdbc.Driver
db.url=jdbc:mysql://localhost:3306/mall
db.user=admin
db.password=secretProperties props = new Properties();
InputStream is = DbUtil.class.getClassLoader()
.getResourceAsStream("database.properties");
props.load(is);
String driver = props.getProperty("db.driver");싱글톤 패턴
지연 초기화 방식
public class ConfigHolderLazy {
private static ConfigHolderLazy instance = null;
private static Properties props = new Properties();
public static synchronized ConfigHolderLazy getInstance() {
if (instance == null) {
instance = new ConfigHolderLazy();
}
return instance;
}
private ConfigHolderLazy() {
try (InputStream is = getClass().getClassLoader()
.getResourceAsStream("database.properties")) {
props.load(is);
} catch (IOException e) {
e.printStackTrace();
}
}
public String get(String key) {
return props.getProperty(key);
}
}즉시 초기화 방식
public class ConfigHolderEager {
private static ConfigHolderEager instance = new ConfigHolderEager();
private static Properties props = new Properties();
static {
try (InputStream is = ConfigHolderEager.class.getClassLoader()
.getResourceAsStream("database.properties")) {
props.load(is);
} catch (IOException e) {
e.printStackTrace();
}
}
private ConfigHolderEager() {}
public static ConfigHolderEager getInstance() {
return instance;
}
public String get(String key) {
return props.getProperty(key);
}
}적용 예시
public Connection obtainConnection() throws Exception {
Properties p = new Properties();
try (InputStream is = DbUtil.class.getClassLoader()
.getResourceAsStream("database.properties")) {
p.load(is);
}
Class.forName(p.getProperty("db.driver"));
return DriverManager.getConnection(
p.getProperty("db.url"),
p.getProperty("db.user"),
p.getProperty("db.password"));
}서블릿 개요
JSP는 내부적으로 서블릿으로 변환됩니다.
생명주기
인스턴스 생성 → init() 초기화 → service() → doGet/doPost → destroy()
| 단계 | 주체 | 시점 |
|---|---|---|
| 인스턴스 생성 | 컨테이너 | 시작 또는 첫 요청 |
| 초기화 | 컨테이너 | 생성 직후 |
| 요청 처리 | 컨테이너 | 요청 수신 시 |
| 소멸 | 컨테이너 | 애플리케이션 종료 |
상속 계층
- Servlet 인터페이스 - 기본 규약
- GenericServlet - 프로토콜 독립적 구현
- HttpServlet - HTTP 프로토콜 전용
web.xml 매핑
<context-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</context-param>
<servlet>
<servlet-name>AuthServlet</servlet-name>
<servlet-class>com.store.web.AuthServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>AuthServlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>어노테이션 매핑
@WebServlet("/login")
public class AuthServlet extends HttpServlet {
private String encoding;
@Override
public void init(ServletConfig cfg) throws ServletException {
encoding = cfg.getServletContext().getInitParameter("encoding");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding(encoding);
// ...
}
}