반응형

지난 글(WebSocket)에 이어서 STOMP에 대해 알아보자.

 

 

1. STOMP


STOMP(Simple/Streaming Text Oriented Messaging Protocol)란 클라이언트와 서버 간의 메시지 전송을 위한 텍스트 기반 프로토콜이다.

 

이 프로토콜은 Publish-Subscribe 구조를 기반으로 하며, Spring의 내장 브로커인 SimpleBroker나 외부 메시지 브로커(RabbitMQ, Redis, Kafka 등)를 활용하여 발신자(Publisher)와 수신자(Subscriber) 간의 메시지를 쉽게 주고받을 수 있다.

 

STOMP는 메시징 프로토콜이며, 발행자(Publisher)가 메시지를 생성하면 브로커가 이를 관리하고 구독자(Subscriber)에게 전달한다.

 

 

 

2. STOMP 구조


이전 글에서 WebSocket을 알아보며, 메세지 형식에 대한 규격이 없어 개발자가 직접 정의해야 하는 문제점들을 언급했다. STOMP를 사용하면 형식을 따로 고민할 필요도, 파싱하기 위한 코드를 구현할 필요도 없다.

 

STOMP는 프레임(frame)이라고 해서 Command, header, body로 이미 형식을 정의해 두었다.

frame

COMMAND
header1:value1
header2:value2

Body
  • Command는 메세지 유형을 나타내며 CONNECT, SEND, SUBSCRIBE 등이 있다.
  • Header은 메시지에 대한 부가 정보를 key-value 형태로 제공한다. 대표적으로 'destination' 헤더는 메시지의 목적지를 지정한다.
  • Body에 실제 전송할 데이터 내용을 포함하게 된다.

 

좌: WebSocket, 우: STOMP

 

 

 

3. STOMP 통신 흐름


STOMP 통신 흐름은 발신자, 서버, 브로커, 구독자 간의 상호작용을 통해 이루어진다.

 

STOMP 프로토콜에서 /topic/app 접두사는 다음과 같은 의미를 가진다.

  1. /topic : 이 접두사를 사용하면 메시지가 직접 메시지 브로커로 전달되어 해당 토픽을 구독하고 있는 모든 클라이언트에게 즉시 발행된다.
  2. /app : 이 접두사는 애플리케이션의 @MessageMapping 메서드로 메시지를 라우팅한다. 이 메서드에서 메시지를 처리한 후, 개발자가 명시적으로 SimpMessagingTemplate을 사용하여 /topic으로 메시지를 전송해야 구독자들에게 발행이 된다.

 

따라서, /app으로 들어온 메시지는 서버에서 추가 처리(데이터베이스 저장, 메시지 변환, 인증 등)를 할 수 있는 기회를 제공하며, 이 처리 후에 개발자가 명시적으로 /topic으로 메시지를 전송해야 구독자들에게 발행된다.

 

이러한 구조는 단순히 메시지를 전달하는 경우와 서버에서 추가 로직이 필요한 경우를 유연하게 처리할 수 있게 해준다.

 

 

 

4. Spring STOMP


이전에 WebSocket을 구현해 볼 때 사용했던 WebSocket King Client의 경우 Command, header, body로 이루어진 STOMP의 프레임 타입을 지원하지 않는다. 따라서 STOMP 테스트를 만들어준 블로그를 이용하여 구현해 보자.

 

 

4.1 환경설정

WebSocket과 STOMP를 Gradle에 의존성 추가

gradle

implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.webjars:stomp-websocket:2.3.3'

 

 

4.2 Config

java

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 클라이언트가 구독할 prefix (예: /topic)
        registry.enableSimpleBroker("/topic");
        // 클라이언트가 메시지를 보낼 때 사용할 prefix (예: /app)
        registry.setApplicationDestinationPrefixes("/app");

    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*");
    }
}

STOMP를 사용하면 더 이상 SocketTextHandler와 같은 저수준 WebSocket 핸들러를 직접 구현할 필요가 없다.

 

STOMP는 메시지 라우팅, 구독 관리, 메시지 형식 등을 추상화하여 제공하므로, 개발자는 비즈니스 로직에 더 집중할 수 있다.

 

대신, @MessageMapping 어노테이션을 사용하여 특정 목적지로 오는 메시지를 처리하는 메서드를 정의하게 된다.

WebSocket 때와 마찬가지로 Tester에서도 .withSockJs() 를 통해 SockJs를 활성화할 수 있지만 오류가 날 수 있으니 제거하고 실습하자.

 

 

4.3 Controller

java

@Controller
@RequiredArgsConstructor
public class ChatController {

    private final SimpMessagingTemplate messagingTemplate;

    @MessageMapping("/chat/enter")
    public void enterChatRoom(@RequestBody ChatDto chatDto, SimpMessageHeaderAccessor headerAccessor) {
        Map<String, Object> sessionAttributes = headerAccessor.getSessionAttributes();

        sessionAttributes.put("id", chatDto.getId());
        sessionAttributes.put("roomCode", chatDto.getRoomCode());

        String joinMessage = chatDto.getId() + "님이 입장하셨습니다.";
        messagingTemplate.convertAndSend("/topic/chat/room/" + chatDto.getRoomCode(), joinMessage);
    }

    @MessageMapping("/chat/send")
    public void sendMessage(@RequestBody ChatDto chatDto) {
        String msg = chatDto.getId() + ": " + chatDto.getMessage();
        messagingTemplate.convertAndSend("/topic/chat/room/" + chatDto.getRoomCode(), msg);
    }
}

@MessageMapping 어노테이션이 붙은 메서드는 WebSocketConfig에서 설정한 applicationDestinationPrefixes ("/app")와 결합된다. 따라서 클라이언트에서 "/app/chat/enter"로 보낸 메시지가 이 메서드로 라우팅 된다.

 

enterChatRoom 메서드는 사용자의 채팅방 입장을 처리한다. 세션 속성에 사용자 ID와 roomCode를 저장하고 입장 메시지를 생성한다. 그 후, messagingTemplate을 사용하여 "/topic/chat/room/{roomCode}" 목적지로 메시지를 전송하며 해당 채팅방을 구독 중인 모든 클라이언트에게 전달된다.

 

sendMessage 메서드에서 메세지를 가공하거나 DB에 저장하는 로직을 추가할 수 있다. DB 연결은 생략하고 메세지만 다음과 같이 가공하여 보내보자. String msg = chatDto.getId() + ": " + chatDto.getMessage();

 

 

4.4 Dto

java

@Data
public class ChatDto {
    private String id;
    private String roomCode;
    private String message;
}

DB가 없으니 Entity는 생략하고 Dto만 간단히 만들어보자.

 

 

4.5 SpringBoot + STOMP TESTER

먼저 STOMP TESTER 에 접속 후 창을 2개 켜서 같은 채팅방에 구독하고, 입장 해보자

 

DESTINATION PATH: /app/chat/enter

MESSAGE(JSON): {"id": "User1", "roomCode": "ROOM123"}

위와 같이 /topic/chat/room/ROOM123으로 구독한 후

 

유저의 정보와 채팅방 정보를 담은 메세지를 보내게 되면 채팅방 입장 메세지가 구독한 유저에게 모두 보이게 된다.

이후로는 아래와 같이 JSON 객체에 message 변수를 추가하여 메세지를 보낼 수 있다.

 

DESTINATION PATH: /app/chat/send

MESSAGE(JSON): {"id": "User1", "roomCode": "ROOM123", "message":"Hello"}

 

 

 

마치며,

 

WebSocket이 단순히 클라이언트-서버 간 1:1 통신을 제공하는 반면, STOMP를 사용하면서 메시지 브로커를 통한 중앙 집중식 구조를 가질 수 있게 되었다.

 

이로 인해 메시지의 관리와 분배가 더 체계적으로 이루어질 수 있으며, 개발자는 복잡한 메시징 로직 구현에 집중하기보다는 비즈니스 로직에 더 집중할 수 있다.

반응형

1. Socket


소켓네트워크에서 데이터를 주고받을 수 있도록 네트워크 환경에 연결할 수 있게 만들어진 연결부이다.

 

마치 전화기와 비슷하다. 두 사람이 전화로 대화하려면 각자 전화기가 필요하듯이, 두 컴퓨터 프로그램이 대화하려면 각각 소켓이 필요하다. 따라서 클라이언트 소켓, 서버 소켓으로 구분된다.

 

현재 대부분의 네트워크 통신은 인터넷 프로토콜을 사용하며, 대표적으로 TCP/IP, UDP/IP가 존재한다. TCP와 UDP는 소켓을 통해 구현되는 가장 흔한 프로토콜이다.

 

 

 

2. WebSocket


웹 소켓하나의 TCP 접속에 전이중 통신 채널을 제공하는 컴퓨터 통신 프로토콜로, 소켓 통신에 기반하여 웹 애플리케이션에 맞게 발전한 형태이다. HTTP나 HTTPS 위에서 동작하도록 설계되었으며, 서버와 브라우저 간 연결을 유지한 상태로 추가적인 HTTP 요청 없이 데이터를 교환할 수 있다.

 

 

2.1 HTTP와 WebSocket의 차이

Http와 WebSocket Protocol의 가장 큰 차이는 수립된 커넥션을 어떻게 하느냐이다.

 

Http는 비 연결성 프로토콜로 클라이언트가 요청을 보낼 때마다 연결을 맺고 응답을 받은 후 연결을 끊어버린다.

웹 소켓 이전의 HTTP 기반으로 한 실시간 통신 방식으로 폴링(Polling)롱 폴링(Long Polling)이 있지만, 클라이언트가 요청을 지속적으로 보내야 하는 상황을 벗어나지 못했다.

 

반면에 웹 소켓의 경우 한번 연결을 맺고 나면 어느 한쪽에서 연결을 끊으라는 요청을 보내기 전까지 연결을 유지하기 때문에 매번 연결할 때마다 발생하는 비용을 줄일 수 있다.

 

다시한번 정리하면 HTTP는 요청과 응답이 한 쌍을 이루는 구조로 통신을 한다. 즉, 내가 원하는 어떤 것을 얻기 위해서는 항상 그것을 달라고 요청을 해야했다. 반면, 웹 소켓은 연결이 계속 유지되고 있는 상태이기 때문에, 연결된 채널, 소켓을 통해 상대가 보내오는 메세지를 듣기만 하면 된다.

 

그렇다면 WebSocket은 어떻게 연결되는 것일까?

 

 

2.2 핸드셰이크(handShake)

웹 소켓 커넥션을 만들려면 new WebSocket() 을 호출하면 되며 이때 handShake라는 과정을 HTTP 프로토콜을 사용해 시작한다.

 

핸드 셰이크가 성공적으로 완료되면, 연결은 HTTP에서 WebSocket Protocol로 전환되며 HTTP → ws://, HTTPS → wss:// 로 변경된다.

 

 

 

3. SpringBoot + WebSocket King Client


Chrome 확장 프로그램인 WebSocket King Client에서 WebSocket을 구현해보자.

 

 

3.1 환경설정

  • Chrome에서 WebSocket King Client 설치 후 접속
  • Spring Boot 2.7.18
  • Java 11

 

websocket과 JSON을 사용하기 위해 Gradle에 의존성 추가

gradle

    implementation 'org.springframework.boot:spring-boot-starter-websocket'
    implementation 'org.json:json:20211205'

 

 

3.2 Config

Spring에서 웹 소켓을 사용하려면 클라이언트가 보내오는 통신을 처리할 Handler가 필요하다.

java

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new SocketTextHandler(), "/ws")
                .setAllowedOriginPatterns("*");
    }
}

이후 구현할 Handler를 웹 소켓이 연결될 때 handShake할 주소와 함께 인자로 넣어주면 된다.

CORS 제어를 위해 setAllowedOriginPatterns를 설정하여 특정 도메인에서만 WebSocket 연결을 허용할 수 있다.

.withSockJs() 를 통해 SockJs를 활성화 할 수 있지만 WebSocket Client King에서 오류가 날 수 있으니 제거하고 실습하자.

 

 

3.3 Handler

java

public class SocketTextHandler extends TextWebSocketHandler{

    private final Set<WebSocketSession> sessions = ConcurrentHashMap.newKeySet();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
        for (WebSocketSession s : sessions) {
            s.sendMessage(new TextMessage("user: " + session.getId() + " connected"));
        }
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        JSONObject jsonObject = new JSONObject(payload);
        for (WebSocketSession s : sessions) {
            s.sendMessage(new TextMessage(jsonObject.get("user") + ": " +  jsonObject.get("message")));
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        sessions.remove(session);
        for (WebSocketSession s : sessions) {
            s.sendMessage(new TextMessage("user: " + session.getId() + " disconnected"));
        }
    }
}

 

웹 소켓은 기본적으로 Tex t또는 Binary 타입을 지원한다.

필요에 따라 TextWebSocketHandler 또는 BinaryWebSocketHandler라는 Spring이 기본적으로 제공하는 클래스를 상속하고 구현하면 된다.

 

이때, 메서드 인자로 받아오는 WebSocketSession이 있다. 흔히 말하는 HTTP 세션과는 다른 맥락으로 간단히 말하면 웹 소켓이 연결될 때 생기는 연결정보를 담고 있는 객체라고 보면 된다.

 

핸들러에서 웹 소켓 통신에 대한 처리를 하기 위해 이 세션들을 컬렉션으로 담아 관리하는 경우가 많으며, 위 코드를 보면 커넥션이 맺어질 때 컬렉션에서 웹 소켓 세션을 추가하고 끊어질 때 제거를 하고 있다.

 

추가로 세션이 연결됨과 끊어질 때, 입장과 퇴장 메세지를 보낼 수 있게 설정해놓았다.

 

handleTextMessage는 WebSocket을 통해 텍스트 메세지를 받았을 때 실행되는 핸들러로 받은 메세의 내용(payload, 클라이언트에서 JSON으로 보냈다고 가정)을 각 연결된 세션에 새 메세지를 보내게된다.

 

 

3.4 WebSocket King Client

상단에 CONNECTIONS을 하나 더 늘려 유저 A와 B가 각각 입장하여 채팅을 한다고 테스트해보자.

 

ws://localhost:8080/ws 주소로 연결과 함께 자동으로 부여된 session Id가 출력됨을 확인할 수 있다. /ws는 config에서 설정한 end-point이다.

 

이후 채팅을 JSON 형태로 보내게 되면, Handler에서 ‘user’‘message’ 의 key 값으로 메세지를 처리하는 걸 확인할 수 있다.

 

개발자 도구를 살펴보면 처음 handShake 과정에서 HTTP를 사용하기 때문에 유사한 양의 헤더 정보를 주고받게 되지만 한번 연결이 수립되고 나서는 간단한 Messages들만 오고 가는 것을 확인할 수 있다.

 

 

  • Origin : 클라이언트 origin을 나타낸다.
  • Connection : Upgrade, 클라이언트 측에서 프로토콜을 바꾸고 싶다는 신호를 보냈다는 것을 나타낸다. (http → ws)
  • Upgrade : websocket, 클라이언트 측에서 요청한 프로토콜은 'websocket’이라는 것을 의미한다.
  • Sec-WebSocket-Key : 보안을 위해 브라우저에서 생성한 키로, 서버가 웹 소켓 프로토콜을 지원하는지를 확인하는 데 사용된다.

 

마치며

WebSocket은 실시간 양방향 통신을 제공하는 강력한 기술이지만 몇 가지 한계가 있다.

 

1️⃣ 메세지 구조화

WebSocket은 단순한 텍스트 교환만을 제공하며 메세지 형식에 대한 규격이 없다. 우리는 handler에서 String 값인 payload를 JSON으로 파싱하여 사용하였다. 따라서 복잡한 메시지 패턴이나 로직들을 개발자가 직접 정의해야하고 구현해야 한다.

 

2️⃣ 클라이언트 관리

세션의 ID는 서버가 클라이언트와의 WebSocket 연결될 때 자동으로 생성되는 랜덤한 값이기 때문에 연결된 클라이언트들을 추적하고 관리하는 시스템을 직접 구축해야한다.

 

3️⃣ 메세지 라우팅

특정 주제(Topic)나 채널(Channel)에 따라 메시지를 구분하거나 라우팅하는 기능이 내장되어 있지 않기 때문에, 이를 구현하려면 추가적인 로직과 구조를 설계해야한다.

 

 

 

이러한 어려움을 해결하기 위해 STOMP와 같은 상위 프로토콜을 사용하면 메지 라우팅과 세션 관리를 더 쉽게 구현할 수 있다.

 

다음 글에서는 STOMP에 대해 알아보고 실습해 보는 시간을 가져보자.

 

 

[Spring] STOMP란? (Spring Boot + STOMP TESTER로 구현해보기)

지난 글(WebSocket)에 이어서 STOMP에 대해 알아보자.  1. STOMPSTOMP(Simple/Streaming Text Oriented Messaging Protocol)란 클라이언트와 서버 간의 메시지 전송을 위한 텍스트 기반 프로토콜이다. 이 프로토콜은 Pu

murphytklee.tistory.com

 

반응형

0. Setting


1. node.js 설치

 

2. vue 설치

bash

// (cmd 관리자 실행)
> npm install vue

// 설치확인
> npm vue -v

 

3. vue cli 설치

bash

// (cmd 관리자 실행)
> npm install -g @vue/cli c

// 설치확인
> vue -V

 

 

 

1. Spring Boot Project Initialize


Spring Initializr를 통해 Spring Boot 프로젝트를 생성

 

  •  개발환경
    • Visual Studio Code
    • Java 11
    • Spring Boot 2.7.17

 

 

 

2. Controller


java

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

    @GetMapping("/")
    public String index() {
        return "index.html";
    }
}
  • Vue.js 가 Build 될 때 생성된 index.html을 return 한다.

 

 

 

3. Vue 프로젝트 생성


  • src/main/java처럼 src/main/vue 로 폴더 구조를 가져가기 위해
  • 관리자 권한으로 실행한 cmd에서 프로젝트의 src\main 로 이동하여 vue.cmd create [프로젝트 명]

bash

src\main> vue.cmd create vue --no-git // src\main에서 폴더와 함께 설치

 

 

vue create 사용 시

  1. 프로젝트 폴더 구조 생성
  2. 필요한 종속성 설치
  3. git init 명령어를 사용하여 프로젝트 폴더에 Git Repository를 초기화 하게 되는데,

--no-git 은 git init을 건너뛰게 된다.

 

 

이후 Vue 버전 선택 하게되면 아래와 같이 Vue 프로젝트가 생성된다.

 

Vue.js를 빌드하면 Spring Boot 내 프로젝트로 빌드 파일이 생성되며, Spring Boot 배포 파일들과 함께 웹 서버로 배포하는 방식이다.

 

 

 

 

4. Config & Build


src\main\vue 에서 Build Directory와 devServer의 proxy를 설정하기 위해 vue.config.js 수정해준다.

 

javascript

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  outputDir: '../resources/static', // Build Directory
    devServer: {
        proxy: 'http://localhost:8080' // Spring Boot Server
    }
})

 

이후 Vue 프로젝트를 cmd에서 Build해준다.

 

bash

>npm run build

 

빌드 후 outputDir 경로에 .vue 파일들이 html, css, js 파일로 변환되고, Spring을 실행하여 localhost:8080 로 접속해보면 Vue로 생성된 화면이 출력되는 것을 확인할 수 있다.

 

App.vue 파일이 메인 페이지에 해당하기 때문에 앞으로는 해당 파일에 코드를 작성해주면 된다.

 

 

 

5. npm run serve


  • 스프링 부트 애플리케이션은 별다른 설정이 없으면 8080 포트를 사용하게 된다. 따라서, Vue 개발용 서버는 따로 설정해주지 않으면 자동적으로 8081, 8082, 8083... 포트를 사용하게 된다.
  • 매번 npm run build 하여 변경사항을 확인할 수 없기에, React와 같이 Hot Reload 기능을 지원하는

npm run serve로 (8081) 개발하면 된다.

 

 

6. Vue Extention Tip


코드 하이라이팅, 오류 검사, 코드 포맷, 디버깅, 자동완성 등을 지원하는 VS Code Extention은 다음과 같다.

  • Vetur
  • Vue - Official : 
  • vue3-snippets-for-vscode

Vetur 설치시 발생하는

vetur can't find ‘tsconfig.json’ or ‘jsconfig.json’ 이나

vetur can't find "package.json' 에러에 대한 해결방법은 해당 포스트 참고

 

[Error] vetur can't find package.json 에러

0. ProblemSpring + Vue 프로젝트 구축 후, VS Code로 Vue 프로젝트를 열었을 때, IDE에서 Vue 문법을 인식하지 못하며 빨간 줄과 함께 해당 오류가 발생했다.vetur can't find tsconfig.json or jsconfig.jsonvetur can't find

murphytklee.tistory.com

 

반응형

처음 Spring에 입문했을 때, Spring Data JPA를 바로 사용하게 되면서 Repository에서 구현 클래스 없이 Interface만으로 개발을 할 수 있었다. 공부를 하면서 JPA, ORM, Hibernate, JDBC와 같은 개념들이 등장했고, 이들이 어떻게 구현되고 있는지, 위 사진을 이해하는 것이 목표이다.

 

가장먼저 ORM을 알아야 하지만, 이해를 돕기위해 RDB를 먼저 짚고 넘어가자.

 

 

 

0. RDB와 OOP


관계형 데이터베이스는 테이블, 행, 열의 정보를 구조화하여 데이터 간의 관계를 설정할 수 있는 데이터베이스 모델이다.

 

전반적으로 관계형 데이터베이스는 강력하고 효율적인 데이터 저장 및 검색 기능을 제공하지만 복잡한 객체 지향 개념을 모델링(상속, 다형성, 레퍼런스, 오브젝트)하는 데 적합하지 않다.

 

우리는 객체 지향 언어(Java)를 사용하고 있는데 RDB는 객체 지향을 모델링하는데 적합하지 않다?

 

이 말은 관계형 데이터베이스와 객체 지향 언어가 서로 다른 개념을 기반으로 설계되었기 때문에 직접적인 매핑이 어렵다는 의미이다.

 

객체 지향 프로그래밍(OOP) 관계형 데이터베이스
(RDB)
불일치 문제
객체(Object) 테이블(Table) 객체는 상태와 행동을 가지지만, 테이블은 행동 없이 데이터(값)만 저장
필드(Field) 컬럼(Column) 기본적으로 매핑 가능하지만, 복잡한 구조(예: 리스트, 객체 포함)는 변환이 필요
객체 참조(Reference) 외래 키(Foreign Key) 객체의 참조는 포인터 개념이지만, RDB에서는 FK로 조인해야 함
상속(Inheritance) 테이블에는 직접적인 개념 없음 OOP의 상속 구조를 테이블에 표현하려면 별도 설계(싱글 테이블 전략, 조인 전략 등)가 필요
다형성(Polymorphism) SQL에서 지원 X SQL에서 다형적 관계를 표현하려면 테이블 구조를 맞춰야 함 (지원 X)

 

 

결론적으로 왜 Java + RDB 조합이 흔한가?

 

Java와 RDB는 서로 다른 패러다임이지만, 기업 환경에서 가장 안정적이고 확장성이 뛰어난 조합이기 때문이다.

1️⃣ RDB는 표준 데이터 저장 방식이고, Java는 기업 시스템에서 가장 많이 쓰이는 언어

2️⃣ ORM을 통해 객체-관계 불일치 문제를 해결할 수 있음

3️⃣ Java의 성능, 안정성, 멀티쓰레딩RDB의 강력한 무결성, 확장성이 만나 기업 시스템에 최적화됨

4️⃣ Spring, Hibernate, MyBatis 같은 프레임워크가 Java + RDB 조합을 더욱 강력하게 지원

 

결국, 현실적인 이유로 Java와 RDB는 궁합이 잘 맞는 조합이 되었다.

 

따라서 Java와 RDB의 패러다임 차이(객체-관계 불일치 문제)를 해결하기 위해 ORM이 등장했다.

 

 

 

1. ORM


ORM(Object Relational Mapping)의 핵심은 ‘객체와 데이터베이스 테이블을 매핑하여, SQL을 직접 작성하지 않고 객체를 통해 데이터를 조작할 수 있도록 하는 기술’이다. 즉, 내가 코드 상에서 생성한 객체가 DB상에 어떤 테이블과 연결이 된다는 것을 의미한다. 이렇게 되면 내가 객체를 조작함으로써 DB를 조작할 수 있게 된다.

 

우리는 JPA에서 DB에 대한 접근을 시도할 때 직접 sql 쿼리문을 만들지 않는다. 다만 객체를 이용한 메소드를 통해 이를 관리할 뿐이다. 예를 들어, SELECT * FROM user 대신 userRepository.findAll() 같은 메서드를 호출하면, ORM이 내부적으로 SQL을 생성하여 실행해 준다.

 

ORM을 사용하면 객체와 테이블 간의 매핑을 통해 객체를 조작하는 것만으로 자동으로 SQL이 생성되며, 개발자는 비즈니스 로직에 집중할 수 있어 생산성이 매우 높아진다.

 

ORM 프레임워크 종류

  • JAVA : JPA, Hibernate, EclipseLink, DataNucleus, Ebean 등

 

 

위 사진을 보면 Data Access Layer들이 곧 Service Layer와 DataBase사이를 연동해주고 있다. JPA, Hibernate은 Java의 ORM 프레임워크인 셈이다. 그렇다면 JDBC API는 ORM인가?

 

 

 

2. JDBC


JDBC(Java Database Connectivity)는 ORM이 아닌, 자바에서 데이터베이스에 연결하고 작업을 수행하기 위한 표준 인터페이스이다.

 

Java는 DBMS의 종류에 상관없이 JDBC API를 통해 데이터베이스 작업을 처리할 수 있다. JDBC를 사용하면 데이터베이스에 접근하여 SQL을 실행하고, 데이터를 삽입(INSERT), 조회(SELECT), 수정(UPDATE), 삭제(DELETE)하는 CRUD 작업을 효율적으로 수행할 수 있다.

 

과거에는 MySQL, Oracle, PostgreSQL 등 각 DBMS마다 다른 방식의 SQL을 사용해야 했기 때문에 개발이 번거로웠다.

이러한 문제를 해결하기 위해 JDBC는 메서드와 전역 변수를 하나의 표준 문법으로 통일하여, DBMS에 관계없이 일관된 방식으로 데이터베이스를 다룰 수 있도록 했다.

 

JDBC 핵심: DBMS의 종류에 상관없이 하나의 JDBC API를 이용하여 일관된 문법으로 데이터베이스 작업을 처리할 수 있다.

JDBC Architecture

JDBC는 Java 애플리케이션과 다양한 DBMS를 연결하는 계층적인 구조로 이루어져 있다.

 

 

JDBC 아키텍처 구성 요소

1️⃣ JDBC API (Application Programming Interface)

  • 개발자가 직접 사용하는 JDBC 인터페이스
  • Connection, Statement, ResultSet 등의 클래스를 제공하여 데이터베이스와 통신

2️⃣ JDBC Driver Manager (드라이버 관리자)

  • 애플리케이션과 각 DBMS용 드라이버를 연결하는 역할
  • 여러 개의 JDBC 드라이버를 관리하며, 적절한 드라이버를 선택하여 연결을 수행

3️⃣ JDBC Driver (JDBC 드라이버)

  • 각 DBMS(MySQL, Oracle 등)에서 제공하는 JDBC 구현체
  • JDBC API를 사용해 작성된 코드가 각 DBMS에 맞는 SQL로 변환되도록 지원

4️⃣ DBMS (Database Management System)

  • MySQL, Oracle, PostgreSQL, MS SQL Server 등의 데이터베이스
  • JDBC 드라이버를 통해 SQL을 실행하고 데이터를 반환

 

 

JDBC와 ORM의 차이점

구분 JDBC ORM (JPA, Hibernate)
SQL 작성 여부 직접 SQL 작성 필요 SQL 자동 생성
데이터 조작 방식 SQL을 이용한 절차적 접근 객체를 이용한 선언적 접근
생산성 SQL을 직접 작성해야 하므로 상대적으로 낮음 자동화된 SQL 처리로 생산성이 높음
유연성 SQL을 직접 제어 가능 ORM 설정에 따라 자동 최적화됨
러닝 커브 쉽고 직관적 ORM 개념을 익혀야 함

 

🚀 JDBC는 ORM의 기반이 되는 기술이며, ORM은 JDBC를 활용하여 더 높은 수준의 데이터 접근 방식을 제공하는 기술이다.

 

 

그럼 또 다시 아래 그림을 보자, 이제 Service, Data Aceess, DB와의 연관 관계를 어느정도 알았다.

 

그러면 JPA와 Hibernate 는 무엇이고 어떻게 객체를 DB와 연동하는 것인가?

 

 

 

 

3. JPA


JPA(Java Persistence API)는 Hibernate 기반의 자바 ORM 기술 표준으로, 자바 애플리케이션에서 관계형 데이터베이스를 다루는 방식을 정의한 인터페이스이다. JPA는 어떻게 동작할까?

JPA는 영속성 컨텍스트(EntityManager)를 통해 Entity를 관리한다. 사용자가 Entity에 대한 CRUD 작업을 수행하면, JPA는 해당 Entity와 매핑된 테이블에 대한 SQL 쿼리를 자동 생성하여, 적절한 시점에 JDBC API를 통해 데이터베이스에 반영한다.

 

 

이해를 돕기 위해 영속성 컨텍스트 개념을 먼저 살펴보자.

영속성 컨텍스트(Persistence Context)란 엔티티를 저장하고 관리하는 논리적 메모리 공간이다.

 

왜 영속성 컨텍스트를 사용할까?

 

데이터베이스와의 통신 횟수를 줄이고, 성능을 최적화하기 위해 사용한다. 마치 메모리 캐시를 활용하여 중복 연산을 줄이는 것과 같은 개념이다. 이를 통해 불필요한 쿼리 실행을 최소화하고, 성능을 향상시킬 수 있다.

 

영속성 컨텍스트를 활용하면 얻을 수 있는 주요 이점은 다음과 같다.

 

 

1️⃣ 1차 캐시 (First Level Cache)

JPA는 엔티티를 조회할 때 1차 캐시에 저장해두고, 이후 동일한 엔티티를 조회할 경우 DB가 아닌 1차 캐시에서 가져온다.

java

// A. 캐시 없이 매번 DB 조회하는 경우
User user1 = entityManager.find(User.class, 1L); // DB에서 조회
User user2 = entityManager.find(User.class, 1L); // DB에서 다시 조회 (비효율적)

// B. JPA 1차 캐시 활용
User user1 = entityManager.find(User.class, 1L); // DB에서 조회 후 1차 캐시에 저장
User user2 = entityManager.find(User.class, 1L); // 1차 캐시에서 조회 (DB 쿼리 발생 X)

 

 

 

2️⃣ 쓰기 지연 (Write Behind, Write Delay)

빈번한 쓰기 작업을 하게 되면 context switching이 여러번 발생할 것이다. 따라서 이러한 Context Switching에 대한 overhead를 줄여주는게 쓰기 지연이다.

 

즉, 쓰기를 원하는 entity를 저장해 두었다가 한번에 처리한다는 것을 의미한다.

 

JPA는 INSERT/UPDATE/DELETE 쿼리를 즉시 실행하지 않고, 트랜잭션을 커밋하는 시점에 한꺼번에 처리한다. JPA는 여러 개의 변경 작업을 모아뒀다가 한 번에 처리하여 성능을 최적화한다.

java

EntityManager entityManager = emf.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin(); // 트랜잭션 시작

entityManager.persist(memberA);
entityManager.persist(memberB);
entityManager.persist(memberC);

// INSERT SQL이 즉시 실행되지 않음

transaction.commit(); // 트랜잭션을 커밋하는 순간, 한꺼번에 INSERT 실행

 

 

 

3️⃣ 변경 감지 (Dirty Checking)

JPA는 엔티티의 변경 사항을 자동으로 감지하고, 트랜잭션이 커밋될 때 변경된 부분만 UPDATE 쿼리를 실행한다. 이를 위해 JPA는 영속성 컨텍스트(1차 캐시)에서 엔티티의 스냅샷(초기 상태)을 유지하며, 트랜잭션이 끝날 때 변경 여부를 비교한다.

 

이는 다음과 같은 장점이 있다.

  • 자동 변경 감지: 직접 UPDATE SQL을 작성하지 않아도 자동으로 변경 사항이 반영됨
  • 성능 최적화: 변경된 필드만 UPDATE 실행 (전체 컬럼 업데이트 방지)
  • 객체 중심 개발 가능: 데이터 변경을 신경 쓰지 않고 객체 필드만 수정하면 자동 반영
  • 트랜잭션 기반 처리: 하나의 트랜잭션 내에서 변경 사항을 모아두었다가 한 번에 처리

java

// A. Dirty Checking 사용 (자동 감지)
User user = entityManager.find(User.class, 1L);
user.setName("New Name"); // 자동 변경 감지
transaction.commit(); // 변경 감지 후 UPDATE 실행

// B. 명시적인 UPDATE 사용
User user = entityManager.find(User.class, 1L);
user.setName("New Name");

entityManager.createQuery("UPDATE User u SET u.name = :name WHERE u.id = :id")
    .setParameter("name", "New Name")
    .setParameter("id", 1L)
    .executeUpdate(); // 명시적으로 UPDATE 쿼리 실행

 

 

 

영속성 컨텍스트의 주요 개념을 살펴보았으니, 이제 이를 관리하는 핵심 객체인 EntityManagerFactoryEntityManager에 대해 알아보자.

 

JPA에서 EntityManagerFactory는 영속성 컨텍스트를 생성하고 관리하는 역할을 하며, EntityManager는 개별 트랜잭션 단위에서 엔티티의 생명주기를 관리하는 중요한 객체이다.

 

그렇다면 EntityManager는 언제, 어떻게 생성되고 사용될까?

 

 

EnitityManagerFactory ➡ WAS가 시작되는 시점에 만들어지고 각 트랜잭션에 대해 새로운 EntityManager를 생성하고 관리하는 Object이다. Transaction 단위를 수행할 때마다 생성한다. 즉, 고객의 요청이 올 때마다 사용했다가 닫는다. 따라서 Thread당 생성이 되고 이에 따라 Thread당 공유가 되지 않는다.

 

 

EntityManager ➡ Entity를 생명주기를 관리하는 context로 Persistence Context역할을 수행해준다. 우리가 Entity를 관리한다는 것을 EntityManger가 Entity에 대한 생명주기를 관리한다는 것을 의미할 것이다. 마치 메모리에서 데이터를 언제 올리고 어떻게 내릴지에 관해 관리하는 것처럼 말이다. 이러한 Entity에 대한 Life Cycle을 정리하면 (비영속, 영속, 준영속, 삭제)가 있다.

 

 

❓그러면 JPA는 모두 트랜잭션일 때 사용이 가능한가?

A: 아니다. Spring에서 트랜잭션 없이 JPA를 사용할 수 있지만 일반적으로 데이터 일관성과 무결성을 보장하기위해 트랜잭션을 활성화 하는 것이 좋다.

 

❓그러면 거의 모든 CRUD 서비스 단에서는 트랜잭션을 활성화 해야겠네?

A: 맞다. 대부분 CRUD 작업을 수행할 때 데이터 일관성 무결성을 보장하기 위해 트랜잭션이 필요하다. 하지만 Read 와 같이 읽기만 하는 경우 @Transactional(readOnly = true)를 설정하여 조회용으로 가져온 Entity의 예상치 못한 수정을 방지할 수 있다. 또한, readOnly = true를 설정하게 되면 JPA는 해당 트랜잭션 내에서 조회하는 Entity는 조회용임을 인식하고 변경 감지를 위한 Snapshot을 따로 보관하지 않으므로 메모리가 절약되는 성능상 이점 역시 존재한다.

 

 

JPA는 영속성 컨텍스트인 EntityManager를 통해 Entity를 관리하고 이러한 Entity가 DB와 매핑되어 사용자가 Entity에 대한 CRUD를 실행을 했을 때 Entity와 관련된 테이블에 대한 적절한 SQL 쿼리문을 생성하고 이를 관리하였다가 필요시 JDBC API를 통해 DB에 날리게 된다.

 

다시 위 문장을 보고 정리를 하자면,

  • JPA는 영속성 컨테스트를 사용하여 db와의 통신을 효율적으로 관리한다.
  • 이러한 영속성 컨텍스트에 대한 JPA상에서 구현체를 EntityManger라고 한다.
  • 이러한 EntityManger는 Entity에 대한 생명주기를 관리하고 db와의 연결정보를 저장해둔다.
  • Entity에 대한 CRUD는 연결정보를 바탕으로 JPA가 자동으로 생성해준다.
  • 이러한 쿼리문은 EntityManger가 관리를 하다가 필요시 처리하게 된다.

 

JPA는 애플리케이션과 JDBC 사이에서 동작한다. JPA는 데이터베이스와 객체를 매핑하는 기술일 뿐, 내부적으로는 데이터베이스와의 통신을 위해 JDBC를 사용한다. 개발자가 JPA를 사용하면, JPA 내부에서 JDBC API를 사용하여 SQL을 호출하여 DB와 통신한다.

 

 

 

또한, JPA도 JDBC와 마찬가지로 인터페이스이기 때문에 구현체가 필요하고, 그 구현체 중 하나가 Hibernate이다.

 

 

 

지금까지의 내용을 그림을 통해 다시 정리를 하고 Hibernate로 넘어가보자.

 

잠깐 그러면 Spring Data JPA는 뭐지 ?

 

 

 

4. Spring Data JPA


Spring Data JPA는 JPA를 더 쉽게 사용하기 위해 JPA 위에 구축된 상위 프레임워크로 데이터 접근 계층을 더욱 간편하게 구성할 수 있도록 돕는 기술이다.

 

1️⃣ Repository 인터페이스를 활용한 쿼리 자동 생성

  • Spring Data JPA는 Repository의 메소드를 통해 쿼리를 날릴 수 있다.
  • 이는 JPA를 한 단계 추상화시킨 Repository라는 인터페이스를 제공함으로써 이루어진다.
  • 사용자가 Repository 인터페이스에 정해진 규칙대로 메소드를 입력하면, Spring이 알아서 해당 메소드 이름에 적합한 쿼리를 날리는 구현체를 만들어서 Bean으로 등록해준다.

2️⃣ 복잡한 쿼리 작성 지원

  • 단순한 Method 기반 쿼리 생성 외에도 @Query, QueryDSL를 사용하여 SQL을 구현할 수 있다.

java

interface UserRepository extends JpaRepository<User, Long> {
    // 1. Method
    List<User> findByName(String name);  // SELECT * FROM user WHERE name = ?

    // 2. @Query
    @Query("SELECT u FROM User u WHERE u.email = :email")
    User findByEmail(@Param("email") String email);
}

 

간단하게 설명하면

  • 메서드명 규칙을 기반으로 JPA 쿼리를 자동 생성하여 코드량을 줄이고 생산성을 향상시키는 프레임워크
  • 추가적인 SQL 작성이 필요한 경우 @Query 애너테이션 또는 Querydsl을 사용하여 해결 가능

 

 

 

5. Hibernate


Hibernate는 Java 기반의 ORM 프레임워크이며, JPA의 구현체 중 하나이다.

 

즉, JPA의 인터페이스를 직접 구현하며, 내부적으로 JDBC API를 활용하여 동작한다.

 

JPAHibernate는 마치 Java의 Interface와 이를 구현하는 class의 관계와 유사하다.

JPA는 규칙(인터페이스)을 정의하고, Hibernate는 이를 구현(Implementation)한 라이브러리이다.

JPA와 Hibernate의 상속 및 구현 관계

 

JPA의 핵심인EntityManagerFactory, EntityManager, EntityTransaction을 Hibernate에서는 각각 SessionFactory, Session, Transaction으로 상속받고 각각 Impl로 구현하고 있음을 확인할 수 있다.

 

“Hibernate는 JPA의 구현체이다”로부터 도출되는 중요한 결론 중 하나는 JPA를 사용하기 위해서 반드시 Hibernate를 사용할 필요가 없다는 것이다.

 

Hibernate의 작동 방식이 마음에 들지 않는다면 언제든지 DataNucleus, EclipseLink 등 다른 JPA 구현체를 사용해도 되고, 심지어 본인이 직접 JPA를 구현해서 사용할 수도 있다.

 

다만 그렇게 하지 않는 이유는 단지 Hibernate가 굉장히 성숙한 라이브러리이기 때문일 뿐이다.(오랜 기간 검증된 안정적인 라이브러리, JPA의 표준을 기반으로 동작)

JPA, Hibernate Architecture

 

쉽게 얘기하면 Hibernate는 JPA에서 메서드로 요청된 정보들을 JDBC가 잘 알아들을 수 있게 한번 더 표준화 작업을 해주고 JDBC에게 넘겨준다.

 

스프링 부트 프로젝트의 External Liberies에 hibernate-core 을 찾아보면 구경할 수 있다.

 

이후 실무에서 JPA를 쓰지않으면 어떻게할까 궁금하여 찾아보았다.

2019.3.3 - "현재 저는 회사에서 각기 다른 환경을 가지고 있는 세 시스템을 동시에 다루고있습니다."

  • 유통시스템 : 마이플랫폼 + Spring framerwork(java) - 옛 DI구조 + mybatis + oracle
  • SCM시스템 : JSP,js,css + Spring framework(java) - 어노테이션구조 + mybatis + oracle
  • 자판기시스템 : 넥사크로 + Spring framework(java) + hibernate + postegreSql

 

전세계적으로는 hibernate가 가장 많았고, 유난히 동아시아 3국에서 mybatis 점유율이 높았고 JPA의 점유율도 점점 상승하고 있는 것 같다. 

 

JPA가 처음 나온 16~17년도 당시에는 JPA와 mybatis를 혼용해서 쓰는 경우가 많았지만, 현재는 JPA + QueryDSL을 많이 쓰는 추세이다. 아무래도 mybatis 같은경우는 직접 XML로 쿼리를 작성을 해야하는데 오류나 실수로인한 에러를 통해 서비스가 장애를 일으킬 수 있기 때문에 SI 쪽이 아니라면 JPA와 mybatis, hibernate와 같은걸 같이 사용하지 않는 추세인 것 같다. 


+

그렇게 Spring Data JPA과 QueryDSL을 공부하고

SI 회사에 입사해서 JSP,js,css + Spring framework(java) + mybatis + oracle을 하게되었다

반응형

0. Apache Tiles란?


Apache Tiles는 Template Composition Framework로, 웹 어플리케이션의 재사용 가능한 템플릿을 관리하고 구성하는데 사용된다.

페이지의 공통 요소(ex, header, footer, sidebar, ..etc)를 별도의 타일로 정의하고 이를 여러 페이지에서 재사용하는 방식으로 사용된다.

이는 주로 Java 기반의 웹 어플리케이션에서 사용되며, JSP(JavaServer Pages)와 함께 사용되는 경우가 많다.

2021.03.02 기준으로 3.0.8버전까지 나왔으며 더 이상 지원하지 않는다고 한다. 공식 문서

 

 

1. System Requirements (3.0.8)


  • Spring Boot 3.x 이상에서는 tiles 3 지원 하지 않으니 주의
  • JDK 7.0 or above
  • Servlet 2.5 or above
  • JSP 2.1 or above
    공식 문서

 

 

2. Directory Structure


dir

src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── example/
│   │           └── demo/
│   │               ├── DemoApplication.java
│   │               ├── controller/
│   │               │   └── Controller.java
│   │               └── config/
│   │                   └── TilesConfig.java
│   ├── resources/
│   │   ├── static/
│   │   ├── templates/
│   │   └── application.yml
│   └── webapp/
│       ├── resources/
│       │   ├── css/
│       │   └── js/
│       └── WEB-INF/
│           ├── jsp/
│           │   └── index.jsp
│           ├── tiles/
│           │   ├──layout/
│           │   │  └── layout_default.jsp
│           │   ├──view/
│           │   │  ├── footer.jsp
│           │   │  └── header.jsp
│           │   └── tiles.xml
│           └── web.xml
└── test/

 

 

3. 설정 및 사용 (v3.0.5)


3.1 Dependency

gradle

implementation 'org.apache.tiles:tiles-jsp:3.0.5'

maven

<dependency> <!-- tiles -->
    <groupId>org.apache.tiles</groupId>
    <artifactId>tiles-jsp</artifactId>
    <version>3.0.5</version>
</dependency>

<dependency> <!-- javax.jstl -->
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
</dependency>

 

 

3.2 application.yml

yml

spring:
  mvc:
    view:
      prefix: /WEB-INF/jsp/
      suffix: .jsp

Spring MVC는 기본적으로 resources - templates라는 폴더에 있는 index.html을 찾아 렌더링한다.

  • JSP 파일의 위치와 접미사를 정의하여 Spring MVC가 JSP파일을 찾을 수 있도록 설정한다.
  • prefix: JSP 파일이 위치한 디렉토리를 지정
  • suffix: JSP 파일의 접미사를 지정

 

3.3 TilesConfig.java

java

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesView;
import org.springframework.web.servlet.view.tiles3.TilesViewResolver;

@Configuration
public class TilesConfig {

    @Bean
    public TilesConfigurer tilesConfigurer() {
        TilesConfigurer tilesConfigurer = new TilesConfigurer();
        tilesConfigurer.setDefinitions(new String[]{"/WEB-INF/tiles/tiles.xml"});
        tilesConfigurer.setCheckRefresh(true);
        return tilesConfigurer;
    }

    @Bean
    public TilesViewResolver tilesViewResolver() {
        TilesViewResolver viewResolver = new TilesViewResolver();
        viewResolver.setOrder(TilesView.class);
        return viewResolver;
    }
}

TilesConfigurer

  • setDefinitions 메서드를 사용하여 Tiles 정의 파일 경로 설정
  • setCheckRefresh를 true로 설정하여 Tiles 정의 파일이 변경될 때마다 새로고침하도록 설정

TilesViewResolver

  • setViewClass를 사용하여 뷰 리졸버가 사용할 뷰 클래스 타입을 설정 -> Tiles 뷰를 렌더링하는데 사용

 

3.4 layout_default.jsp

jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="tiles" uri="http://tiles.apache.org/tags-tiles" %>
<!DOCTYPE html>
<html>
    <head>
    </head>
    <body>
        <div>
            <!-- header -->
            <tiles:insertAttribute name="header"/>

            <!-- body -->
            <tiles:insertAttribute name="body"/>

            <!-- footer -->
            <tiles:insertAttribute name="footer"/>
          </div>
    </body>
</html>
  • insertAttribute name 은 tiles.xml에서 설정한 put-attribute name과 일치해야한다.

 

3.5 header.jsp / footer.jsp

jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<div class="header">
    <h2>Header</h2>
</div>

jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<div class="footer">
    <h4>Footer</h4>
</div>

 

3.6 tiles.xml

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE tiles-definitions PUBLIC "-//Apache Software Foundation//DTD Tiles Configuration 3.0//EN" "http://tiles.apache.org/dtds/tiles-config_3_0.dtd">
<tiles-definitions>

<!-- [ layout.default ] -->
    <!-- ( LAYOUT ) -->
    <definition name="layout.default" templateExpression="/WEB-INF/tiles/layout/layout_default.jsp">
        <put-attribute name="header" expression="/WEB-INF/tiles/view/header.jsp"/>
        <put-attribute name="body" expression=""/>
        <put-attribute name="footer" value="/WEB-INF/tiles/view/footer.jsp"/>
    </definition>

        <!-- ( EXTENDS ) -->
        <definition name="index" extends="layout.default">
            <put-attribute name="body" expression="/WEB-INF/jsp/index.jsp"/>
        </definition>
        <definition name="*/*" extends="layout.default">
            <put-attribute name="body" expression="/WEB-INF/jsp/{1}/{2}.jsp"/>
        </definition>
        <definition name="*/*/*" extends="layout.default">
            <put-attribute name="body" expression="/WEB-INF/jsp/{1}/{2}/{3}.jsp"/>
        </definition>
        <definition name="*/*/*/*" extends="layout.default">
            <put-attribute name="body" expression="/WEB-INF/jsp/{1}/{2}/{3}/{4}.jsp"/>
        </definition>

</tiles-definitions>
  • definition: Tiles 정의를 나타냄. 각 정의는 특정 레이아웃이나 페이지 구성을 나타냄
    • name : 정의의 이름. 이 이름을 사용하여 Tiles를 참조할 수 있다.
    • templates : 기본 템플릿 파일의 경로 (공통 레이아웃을 정의)
  • put-attribute: 템플릿의 각 부분을 정의한다.
    • name : 자리 표시자
    • value : 자리 표시자에 해당하는 파일의 경로 또는 값
  • 위 코드의 경우 /index 로 들어오는 페이지는 layout.default를 확장하여 정의하며 body 부분은 /WEB-INF/jsp/index.jsp 파일로 설정된다.
  • /* 과 {} 를 통해 들어오는 url에 대해 전역적으로 jsp 파일을 맵핑할 수 있다.

 

3.7 Controller.java

java

package com.example.jspprac.auth.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class Controller {

    @GetMapping("/")
    public String home() {
        return "index";
    }    
}

WEB-INF 안에 있는 JSP 파일에 접근하기 위해서는 Spring MVC 컨트롤러나 서블릿을 사용해야 한다.

WEB-INF는 웹 애플리케이션의 보안 및 구조를 위해 사용되며, 직접 브라우저를 통해 접근할 수 없기 때문이다.

반대로 외부 유저가 접근해도 문제없는, 보안성이 중요하지 않을 경우 WEB 디렉토리를 통해 a태그로 이동하도록 설계해도 된다.

Controller의 return값과 Tiles.xml에서 definition에 정의한 name이 반드시 일치해야 해당 tiles를 탄다.

 

3.8 index.jsp

jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles"%>

 

 

4. Result


최종적으로 index.jsp 는 빈 페이지지만 layout_default를 extend 하고 있기 때문에

jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles"%>
<html>
    <head>
    </head>
    <body>
        <div>
            <!-- header -->
            <tiles:insertAttribute name="header"/>

            <!-- body -->
            <tiles:insertAttribute name="body"/>

            <!-- footer -->
            <tiles:insertAttribute name="footer"/>
          </div>
    </body>
</html>

위와 같이 페이지가 만들어지며,

tiles.xml에 맵핑해 놓은 jsp파일 또한

jsp

<put-attribute name="header" expression="/WEB-INF/tiles/view/header.jsp"/>
<put-attribute name="body" expression=""/>
<put-attribute name="footer" value="/WEB-INF/tiles/view/footer.jsp"/>

반영되고,

최종적으로 아래와 같이 html이 완성된다.

jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles"%>
<html>
    <head>
    </head>
    <body>
        <div>
            <!-- header -->
            <div class="header">
                  <h2>Header</h2>
            </div>

            <!-- body -->

            <!-- footer -->
            <div class="footer">
                <h4>Footer</h4>
              </div>
        </div>
    </body>
</html>
반응형
반응형

+ Recent posts