JPA  ( Java Persistence API )

  • 데이터베이스와 객체 ( 클래스 ) 를 연결해주는 기술
  • SQL 없이도 DB 작업을 할 수 있게 도와주는 ORM 기술
  • 자바에서 표준으로 만든 인터페이스 ( interface )
  • 대표적인 구현체 : Hibernate

 

cf. Mybatis : SQL을 대신 다뤄주는 Mapping 해주는 기술임 -> 우리나라에서 많이 씀, java Spring : 우리나라에서 많이 씀

 

Spring Boot : 필요한 것만 추가해서 가볍게 시작할 수 있음

Spring : 필요없는 것 까지 모두 추가되서 무거울 수 있음

 

# 테이블 자동 생성/수정 기능 끄기
spring.jpa.hibernate.ddl-auto=none
# 실행되는 sql 쿼리를 콘솔에 출력
spring.jpa.show-sql=true
# SQL을 보기 좋게 정렬해서 출력
spring.jpa.properties.hibernate.format_sql=true
# 뷰(View)에서 지연 로딩을 방지(지연로딩 예외 방지용 설정)
# cf. 지연로딩 ? 
spring.jpa.open-in-view=false

 

* 지연로딩 (Lazy Loading)

  • DB에서 데이터를 바로 가져오지 않고, 실제 사용할 때 가져오는 방식
  • jpa에서는 지연로딩이 디폴트
  • 단, 테이블이 join 되어야한 상태로 출력해야한다면, 즉시 로딩으로 바꿔줘야함

 

ex. 회원이 쓴 글 목록이 보고싶다면? 

  • 즉시 로딩 : Member를 불러오면 Posts도 즉시 같이 가져옴
  • 지연 로딩 : Member를 불러올 때는 Posts는 안 가져오고, 사용할 때 가져옴

버전 차이 오류임 문제없음

* Spring Security

  • 자바 웹 애플리케이션에서 로그인, 로그아웃, 권한, 인증 같은 보안 기능을 쉽고 강력하게 처리할 수 있는 프레임워크

* 동작 흐름

  1. 사용자가 /login 요청을 보냄
  2. 스프링 시큐리티가 요청을 가로챔
  3. 사용자의 아이디/비밀번호 확인
  4. 인증 성공 시 세션에 사용자 정보를 저장
  5. 인증된 사용자만 특정 URL에 접근 허용
  6. 권한이 없으면 접근 차단(403) → security에서 막은것
  7.  

password는 Spring Boot에서 줌

 

** 폴더 구조

'Server > String Boot' 카테고리의 다른 글

RESTAPI  (0) 2025.06.19
Thymeleaf  (0) 2025.06.18
스프링 vs 스프링부트  (0) 2025.06.18

https://ko.vite.dev/guide/

 

Vite

Vite, 프런트엔드 개발의 새로운 기준

ko.vite.dev

 

npm create vite@latest restapi --template react

cd restapi
npm install
npm run dev

// 결과
> restapi@0.0.0 dev
> vite


  VITE v6.3.5  ready in 848 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

시작 화면

// vite.config.js 파일

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default {
  server: {
    // 접속할 주소
    proxy:{
      "/api" :'http://localhost:8080'
    }
  }
}
더보기

<DOM> : 화면 흐름이 UI적으로 매우 깔끔함

[ 브라우저 ]  ↔  [ client]

     ↑

[ 메모리]

// main.jsx

import React from "react";
import ReactDom from "react-dom/client";
import App from "./App.jsx";
import { BrowerRouter } from "react-router-dom";

createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <BrowerRouter>
        <App />
    </BrowerRouter>
  </React.StrictMode>
);

 

// App.jsx

import React from 'react'
import {Routes, Route, Navigate} from 'react-router-dom'
import Login from './pages/login' 
import Register from './pages/Register'

export default function App(){
  return (
    // 화면 그리기 --> jsx 문법으로!
    <Routes>
      <Route path="/" element = {<Navigate to="/login"/>}/>
      <Route path="/login" element = {<Login/>}/>
      <Route path="/register" element = {<Register/>}/>

    </Routes>
  )
}

 

// Login.jsx

import React from 'react'
// import {Routes, Route, Navigate} from 'react-router-dom'

export default function Login(){
    
    return(
        <div>
            <h2>로그인</h2>
            <p>아이디 : <input /></p>
            <p>비밀번호 : <input type='password'/></p>
            <button>로그인</button>
            <p></p>
        </div>
    )
}

 

// Register.jsx

import React from 'react'

export default function Register(){
    
    return(
        <div>
            <h2>회원가입</h2>
            <p>아이디 : <input /></p>
            <p>비밀번호 : <input type='password'/></p>
            <p>이름 : <input /></p>
            <button>회원가입</button>
            <p></p>
        </div>
    )
}

'Server > Servlet' 카테고리의 다른 글

MyBatis  (0) 2025.06.18
MVC2  (0) 2025.06.12
Tomcat9  (3) 2025.06.10
Servlet  (0) 2025.06.09
Session & Servlet api  (0) 2025.06.05

build.gradle

BCrypt

BCrypt는 패스워드를 안전하게 암호화하기 위해 널리 사용되는 해시 함수 알고리즘입니다. org.mindrot.jbcrypt.BCrypt는 이 알고리즘을 자바에서 쉽게 사용할 수 있도록 제공하는 라이브러리로, 비밀번호를 해시하여 저장하고, 사용자가 로그인할 때 입력한 비밀번호가 기존 해시와 일치하는지 안전하게 비교할 수 있도록 hashpw()와 checkpw() 메서드를 제공합니다. 이 방식은 단방향 암호화로 복호화가 불가능하고, salt을 자동으로 적용하여 동일한 비밀번호라도 해시값이 달라지기 때문에 무차별 대입 공격(Brute-force)과 레인보우 테이블 공격에 강한 보안성을 갖습니다.

...
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // lombok
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // mysql
    runtimeOnly 'com.mysql:mysql-connector-j'

    // mybatis
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'

    // bcrypt
    implementation 'org.mindrot:jbcrypt:0.4'

    // JSON ,JWT
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

}
...

 

application.properties

spring.application.name=restapi

spring.datasource.url=jdbc:mysql://localhost:3306/spring?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.koreait.restful.dto

 

MemberDTO.java

package com.koreait.restapi.dto;

import lombok.Data;

@Data
public class MemberDTO {
    private int id;
    private String username;
    private String password;
    private String name;
}

 

MemberMapper.java

package com.koreait.restapi.mapper;

import com.koreait.restful.dto.MemberDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface MemberMapper {
    MemberDTO findByUsername(@Param("username") String username);
    void save(MemberDTO member);
    void update(MemberDTO member);
}

 

MemberMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.koreait.restapi.mapper.MemberMapper">

    <select id="findByUsername" resultType="com.koreait.restful.dto.MemberDTO">
        SELECT * FROM member WHERE username = #{username}
    </select>

    <insert id="save" parameterType="com.koreait.restful.dto.MemberDTO">
        INSERT INTO member (username, password, name)
        VALUES (#{username}, #{password}, #{name})
    </insert>

    <update id="update" parameterType="com.koreait.restful.dto.MemberDTO">
        UPDATE member
        SET
        <if test="password != null and password != ''">
            password = #{password},
        </if>
        name = #{name}
        WHERE id = #{id}
    </update>

</mapper>

 

MemberController.java

package com.koreait.restapi.controller;

import com.koreait.restapi.dto.MemberDTO;
import com.koreait.restapi.jwt.JwtUtil;
import com.koreait.restapi.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@CrossOrigin(origins = "http://localhost:5173", allowCredentials = "true") // 프론트 주소
@RestController
@RequestMapping("/api/member")
@RequiredArgsConstructor
public class MemberController {

    private final MemberService service;
    private final JwtUtil jwtUtil;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody MemberDTO loginRequest) {
        String token = service.login(loginRequest.getUsername(), loginRequest.getPassword());
        if (token != null) {
            return ResponseEntity.ok().body(token);
        } else {
            return ResponseEntity.status(401).body("로그인 실패");
        }
    }

    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody MemberDTO member) {
        service.register(member);
        return ResponseEntity.ok("회원가입 성공");
    }

    @GetMapping("/info")
    public ResponseEntity<?> getUserInfo(@RequestHeader("Authorization") String token) {
        MemberDTO member = service.getUserInfoFromToken(token);
        if (member != null) {
            return ResponseEntity.ok(member);
        } else {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("사용자를 찾을 수 없습니다.");
        }
    }

    @PutMapping("/update")
    public ResponseEntity<?> update(@RequestHeader("Authorization") String token,
                                    @RequestBody MemberDTO member) {
        service.update(token, member);
        return ResponseEntity.ok("회원정보 수정 성공");
    }

    @PostMapping("/logout")
    public ResponseEntity<?> logout(@RequestHeader("Authorization") String token) {
        service.logout(token);
        return ResponseEntity.ok("로그아웃 성공 (클라이언트에서 토큰 삭제)");
    }
}

 

JwtAuthFilter.java

package com.koreait.restapi.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    public JwtAuthFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        response.setHeader("Access-Control-Allow-Origin", "http://localhost:5173");
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
        response.setHeader("Access-Control-Allow-Credentials", "true");

        // 프리플라이트 OPTIONS 요청일 경우 바로 응답
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }

        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
            if (!jwtUtil.isTokenValid(token)) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return;
            }
        } else if (!request.getRequestURI().contains("login") && !request.getRequestURI().contains("register")) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        filterChain.doFilter(request, response);
    }
}

 

JwtUtil.java

package com.koreait.restful.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;

@Component
public class JwtUtil {

    private final String SECRET = "MySuperSecretKeyForJWTTokenWhichIsVerySecure12345";
    private final long EXPIRATION = 1000 * 60 * 60; // 1시간

    private final Key key = Keys.hmacShaKeyFor(SECRET.getBytes());

    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }

    public String getUsernameFromToken(String token) {
        return parseClaims(token).getSubject();
    }

    public boolean isTokenValid(String token) {
        try {
            parseClaims(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    private Claims parseClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

 

MemberService.java

package com.koreait.restful.service;

import com.koreait.restful.dto.MemberDTO;

public interface MemberService {
    String login(String username, String password);
    void register(MemberDTO member);
    void update(String token, MemberDTO updatedMember);
    void logout(String token); // optional
    MemberDTO getUserInfoFromToken(String token);
}

 

MemberServiceImpl.java

package com.koreait.restful.service;

import com.koreait.restful.dto.MemberDTO;
import com.koreait.restful.jwt.JwtUtil;
import com.koreait.restful.mapper.MemberMapper;
import lombok.RequiredArgsConstructor;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {

    private final MemberMapper mapper;
    private final JwtUtil jwtUtil;

    @Override
    public String login(String username, String password) {
        MemberDTO member = mapper.findByUsername(username);
        if (member != null && BCrypt.checkpw(password, member.getPassword())) {
            return jwtUtil.generateToken(member.getUsername());
        }
        return null;
    }

    @Override
    public void register(MemberDTO member) {
        String hashed = BCrypt.hashpw(member.getPassword(), BCrypt.gensalt());
        member.setPassword(hashed);
        mapper.save(member);
    }

    @Override
    public void update(String token, MemberDTO updatedMember) {
        String jwt = token.replace("Bearer ", "");
        String username = jwtUtil.getUsernameFromToken(jwt);

        MemberDTO original = mapper.findByUsername(username);
        if (original != null) {
            if (StringUtils.hasText(updatedMember.getPassword())) {
                String hashed = BCrypt.hashpw(updatedMember.getPassword(), BCrypt.gensalt());
                original.setPassword(hashed);
            }
            if (StringUtils.hasText(updatedMember.getName())) {
                original.setName(updatedMember.getName());
            }
            mapper.update(original);
        }
    }

    @Override
    public MemberDTO getUserInfoFromToken(String token) {
        String jwt = token.replace("Bearer ", "");
        String username = jwtUtil.getUsernameFromToken(jwt);
        MemberDTO member = mapper.findByUsername(username);
        if (member != null) {
            member.setPassword(null);
        }
        return member;
    }

    @Override
    public void logout(String token) {
        // Stateless 방식이라 클라이언트에서 토큰 제거하면 됨
    }
}

'Server > String Boot' 카테고리의 다른 글

JPA  (0) 2025.06.30
Thymeleaf  (0) 2025.06.18
스프링 vs 스프링부트  (0) 2025.06.18

타임리프(Thymeleaf)는 자바 기반 웹 애플리케이션에서 사용되는 현대적인 서버 사이드 템플릿 엔진으로, HTML, XML, JavaScript, CSS 등 다양한 마크업 언어와 자연스럽게 통합됩니다. 특히 HTML 파일을 그대로 브라우저에서 열어도 구조를 유지할 수 있어 디자이너와 개발자가 협업하기에 유리하며, ${변수}나 th:text, th:if 같은 속성 기반 문법을 통해 동적인 화면 구성이 가능하고, Spring MVC와도 강력하게 통합되어 컨트롤러에서 넘겨준 데이터를 직관적으로 표현할 수 있습니다.

 

1. 문법 구조

 

<!-- 반복문과 조건문 -->
<ul>
  <li th:each="item : ${items}" th:text="${item}" th:if="${item != '삭제'}"></li>
</ul>

<!-- 폼 제출 -->
<form th:action="@{/register}" method="post">
  <input type="text" th:field="*{username}" />
  <input type="password" th:field="*{password}" />
  <button type="submit">가입</button>
</form>

 

 

2. 회원 가입 예제

테이블

CREATE TABLE member (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(100) NOT NULL,
    name VARCHAR(50) NOT NULL
);

 

build.gradle

...
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'com.mysql:mysql-connector-j'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
...

 

application.properties

application.properties는 Spring Framework에서 애플리케이션의 설정 정보를 정의하는 파일로, 서버 포트, 데이터베이스 연결 정보, 로깅 수준, 경로 설정 등 다양한 환경설정을 key-value 형태로 작성할 수 있습니다. 이 파일은 src/main/resources 디렉토리에 위치하며, Spring Boot는 이 파일의 설정값을 자동으로 읽어와 애플리케이션 구동 시 적용합니다. 이를 통해 코드 변경 없이 운영환경에 맞는 설정을 쉽게 관리할 수 있습니다.

server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root

 

application.yaml은 들여쓰기를 기반으로 한 계층적 구조를 사용하여 설정을 트리 형태로 표현할 수 있어 가독성이 높고 복잡한 설정을 더 명확하게 나타낼 수 있습니다.

server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root

 

spring.application.name=MemberTest

spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.example.member.dto

spring.datasource.hikari.connection-test-query=SELECT 1
spring.datasource.hikari.maximum-pool-size=10

logging.level.com.example=DEBUG
logging.level.org.mybatis=DEBUG

 

member-mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.member.mapper.MemberMapper">

    <insert id="save" parameterType="MemberDTO">
        INSERT INTO members (username, password, name)
        VALUES (#{username}, #{password}, #{name})
    </insert>

    <select id="findByUsername" resultType="MemberDTO">
        SELECT * FROM members WHERE username = #{username}
    </select>

    <update id="update" parameterType="MemberDTO">
        UPDATE members SET password = #{password}, name = #{name}
        WHERE id = #{id}
    </update>

</mapper>

 

MemberDTO.java

@Data는 Lombok 라이브러리에서 제공하는 애노테이션으로, 자바 클래스에 자주 사용하는 보일러플레이트 코드(boilerplate code)를 자동으로 생성해주는 매우 유용한 기능입니다. @Data가 자동 생성하는 요소는 아래와 같습니다.

  1. Getter 메서드 (모든 필드에 대해)
  2. Setter 메서드 (모든 필드에 대해 final이 아닌 경우)
  3. toString() 메서드
  4. equals() 및 hashCode() 메서드
  5. @RequiredArgsConstructor (final 필드나 @NonNull 필드를 매개변수로 받는 생성자)
package com.example.member.dto;

import lombok.Data;

@Data
public class MemberDTO {
    private int id;
    private String username;
    private String password;
    private String name;
}

 

MemberMapper.java

@Mapper는 MyBatis 프레임워크에서 사용되는 애노테이션으로, 인터페이스가 SQL 매퍼임을 명시하는 데 사용됩니다. 이 애노테이션을 통해 MyBatis는 해당 인터페이스를 구현체로 인식하고, XML 매퍼 파일이나 어노테이션 기반 SQL과 연결하여 자동으로 매핑을 처리할 수 있습니다.

  • SQL Mapper 인터페이스 지정
    @Mapper가 붙은 인터페이스는 MyBatis가 자동으로 프록시 객체를 생성하여, 실제 SQL 실행 코드를 자동으로 주입합니다.
  • Spring과의 통합
    Spring Boot와 함께 사용할 경우, @Mapper를 사용하면 Mapper 인터페이스가 자동으로 빈(bean)으로 등록되어 DI(의존성 주입)에 사용할 수 있습니다.
package com.example.member.mapper;

import com.example.member.dto.MemberDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface MemberMapper {
    MemberDTO findByUsername(@Param("username") String username);
    void save(MemberDTO member);
    void update(MemberDTO member);
}

 

빈(Bean)

  • 자바에서 클래스를 만들면 new로 객체를 직접 생성하지만 Spring에서는 필요한 객체를 직접 만들지 않고, Spring이 대신 만들어서 관리해줍니다. 이처럼 Spring 컨테이너가 만들어서 관리하는 객체를 "빈(Bean)"이라고 부릅니다.
@Component
public class MemberService {
    public void print() {
        System.out.println("회원 서비스 실행");
    }
}

 

의존성 주입(DI: Dependency Injection)

  • 어떤 클래스가 다른 클래스를 사용할 때, 그 객체를 직접 만들지 않고 Spring에게 주세요! 라고 요청하는 방식입니다. 이렇게 다른 객체를 필요한 시점에 자동으로 넣어주는 것을 의존성 주입(DI)이라고 합니다.
@Repository
public class MemberRepository {
    public void save() {
        System.out.println("회원 저장됨!");
    }
}

 

@Service
public class MemberService {

    // Spring이 이 부분을 자동으로 채워줌 (의존성 주입)
    @Autowired
    private MemberRepository memberRepository;

    public void join() {
        memberRepository.save();
    }
}

 

MemberController.java

package com.example.member.controller;

import com.example.member.dto.MemberDTO;
import com.example.member.service.MemberService;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberService service;

    @GetMapping("/login")
    public String loginPage() {
        return "login";
    }

    @PostMapping("/login")
    public String login(@RequestParam String username,
                        @RequestParam String password,
                        HttpSession session,
                        Model model) {
        if (service.login(username, password, session)) {
            return "redirect:/home";
        } else {
            model.addAttribute("error", "로그인 실패");
            return "login";
        }
    }

    @GetMapping("/register")
    public String registerPage() {
        return "register";
    }

    @PostMapping("/register")
    public String register(MemberDTO member) {
        service.register(member);
        return "redirect:/login";
    }

    @GetMapping("/home")
    public String home(HttpSession session, Model model) {
        MemberDTO user = (MemberDTO) session.getAttribute("loginUser");
        if (user == null) return "redirect:/login";
        model.addAttribute("user", user);
        return "home";
    }

    @GetMapping("/update")
    public String updatePage(HttpSession session, Model model) {
        MemberDTO user = (MemberDTO) session.getAttribute("loginUser");
        if (user == null) return "redirect:/login";
        model.addAttribute("member", user);
        return "update";
    }

    @PostMapping("/update")
    public String update(MemberDTO member, HttpSession session) {
        service.update(member);
        session.setAttribute("loginUser", member);
        return "redirect:/home";
    }

    @GetMapping("/logout")
    public String logout(HttpSession session) {
        service.logout(session);
        return "redirect:/login";
    }
}

 

MemberService.java

package com.example.member.service;

import com.example.member.dto.MemberDTO;
import jakarta.servlet.http.HttpSession;

public interface MemberService {
    boolean login(String username, String password, HttpSession session);
    void register(MemberDTO member);
    void update(MemberDTO member);
    void logout(HttpSession session);
}

 

MemberServiceImpl.java

@RequiredArgsConstructor

  • 클래스에 있는 final 필드나 @NonNull이 붙은 필드에 대해 생성자(constructor)를 자동으로 생성해주는 Lombok 애노테이션입니다. 즉, 필수 필드만을 매개변수로 받는 생성자를 만들어줍니다.
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class MemberService {
    private final MemberRepository memberRepository;
}

위 코드는 Lombok이 아래 생성자를 자동으로 만들어줍니다

 

public MemberService(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}

 

package com.example.member.service;

import com.example.member.dto.MemberDTO;
import com.example.member.mapper.MemberMapper;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {

    private final MemberMapper mapper;

    @Override
    public boolean login(String username, String password, HttpSession session) {
        MemberDTO member = mapper.findByUsername(username);
        if (member != null && StringUtils.hasText(password) && password.equals(member.getPassword())) {
            session.setAttribute("loginUser", member);
            return true;
        }
        return false;
    }

    @Override
    public void register(MemberDTO member) {
        mapper.save(member);
    }

    @Override
    public void update(MemberDTO member) {
        mapper.update(member);
    }

    @Override
    public void logout(HttpSession session) {
        session.invalidate();
    }
}

 

login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>로그인</title>
</head>
<body>
<h2>로그인</h2>
<form th:action="@{/login}" method="post">
  <p>아이디: <input type="text" name="username" /></p>
  <p>비밀번호: <input type="password" name="password" /></p>
  <p><button type="submit">로그인</button></p>
</form>
<p style="color:red;" th:if="${error}" th:text="${error}"></p>
<p><a th:href="@{/register}">회원가입</a></p>
</body>
</html>

 

register.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>회원가입</title>
</head>
<body>
<h2>회원가입</h2>
<form th:action="@{/register}" method="post">
  <p>아이디: <input type="text" name="username" /></p>
  <p>비밀번호: <input type="password" name="password" /></p>
  <p>이름: <input type="text" name="name" /></p>
  <p><button type="submit">가입</button></p>
</form>
<p><a th:href="@{/login}">로그인으로</a></p>
</body>
</html>

 

home.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>홈</title>
</head>
<body>
<h2 th:text="'환영합니다, ' + ${user.name} + '님!'"></h2>
<ul>
  <li>아이디: <span th:text="${user.username}"></span></li>
  <li>이름: <span th:text="${user.name}"></span></li>
</ul>
<p><a th:href="@{/update}">회원정보 수정</a></p>
<p><a th:href="@{/logout}">로그아웃</a></p>
</body>
</html>

 

update.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>회원정보 수정</title>
</head>
<body>
<h2>회원정보 수정</h2>
<form th:action="@{/update}" method="post">
  <input type="hidden" name="id" th:value="${member.id}" />
  <p>아이디: <input type="text" name="username" th:value="${member.username}" readonly /></p>
  <p>비밀번호: <input type="password" name="password" /></p>
  <p>이름: <input type="text" name="name" th:value="${member.name}" /></p>
  <p><button type="submit">수정</button></p>
</form>
<p><a th:href="@{/home}">홈으로</a></p>
</body>
</html>

 

'Server > String Boot' 카테고리의 다른 글

JPA  (0) 2025.06.30
RESTAPI  (0) 2025.06.19
스프링 vs 스프링부트  (0) 2025.06.18

스프링(Spring)은 자바 플랫폼을 위한 오픈소스 프레임워크로, 객체 지향 설계를 기반으로 의존성 주입(DI)과 관점 지향 프로그래밍(AOP) 같은 기능을 통해 개발자의 생산성과 유지보수성을 높여주는 반면, 설정이 복잡하고 XML 설정 파일이나 자바 설정 클래스가 많이 필요하다는 단점이 있습니다. 이에 비해 스프링부트(Spring Boot)는 스프링을 더욱 쉽게 사용할 수 있도록 만든 확장 프레임워크로, 자동 설정(Auto Configuration)과 내장 톰캣, 스타터 의존성 관리 등을 통해 개발자가 빠르게 애플리케이션을 구축할 수 있도록 도와줍니다.

 

 

1. IntelliJ에서 Spring 프로젝트 만들기 (Spring Boot 기준)

 

1. 프로젝트 생성

  1. IntelliJ 실행 → New Project 클릭
  2. 왼쪽에서 Spring Initializr 선택
  3. SDK는 설치된 Java 버전 선택 (예: 17)
  4. Project Metadata 입력:
    • Group: com.example
    • Artifact: myapp ← 프로젝트 이름
  5. Next 클릭 후 필요한 Dependency 선택
    • Spring Web (웹 개발)
    • Spring Boot DevTools (자동 재시작)
    • Lombok (선택사항)

 

2. 프로젝트 구조

myapp/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/myapp/
│   │   │       ├── MyappApplication.java   <-- 🔸 시작 클래스
│   │   │       ├── controller/             <-- 🔹 요청을 받는 클래스
│   │   │       ├── service/                <-- 🔹 비즈니스 로직 처리
│   │   │       └── repository/             <-- 🔹 DB 접근 계층 (JPA 등)
│   │   └── resources/
│   │       ├── static/                     <-- 🔹 정적 파일 (HTML, CSS, JS 등)
│   │       ├── templates/                  <-- 🔹 Thymeleaf 등의 템플릿 파일
│   │       └── application.properties      <-- 🔸 설정 파일
│
├── build.gradle OR pom.xml                <-- 의존성 관리 파일 (Gradle or Maven)
└── ...

 

MyappApplication.java 애플리케이션의 시작점 (@SpringBootApplication)
controller/ 사용자 요청을 처리하는 클래스 (예: /hello)
service/ 실제 로직 처리 (예: 계산, 비즈니스 처리)
repository/ 데이터베이스와의 연결 담당 (JPA, MyBatis 등 사용)
static/ HTML, CSS, JS 같은 정적 파일 위치
templates/ 뷰 템플릿 파일 (Thymeleaf 등) 위치
application.properties / yml 포트번호, DB연결 등 설정 파일
build.gradle or pom.xml 필요한 라이브러리(의존성)를 정의하는 곳

 

 

2. Gradle

Gradle은 자바를 비롯한 여러 프로그래밍 언어 프로젝트의 빌드 자동화 도구로, 코드 컴파일, 테스트, 패키징, 의존성 관리 등을 손쉽게 처리해주는 도구입니다. 기존의 Maven보다 더 빠르고 유연하게 동작하며, Groovy 또는 Kotlin DSL 문법을 사용해 빌드 스크립트를 작성합니다. Gradle은 필요한 라이브러리를 자동으로 다운로드하고, 프로젝트를 실행 가능한 형태(jar, war 등)로 만들어주는 역할을 하며, 특히 Spring Boot와 함께 사용할 때 매우 강력하고 편리한 도구로 널리 사용됩니다.

 

1. 파일 구조

myapp/
├── build.gradle               <-- 🔸 가장 핵심적인 빌드 스크립트 파일
├── settings.gradle           <-- 프로젝트 이름 설정
├── gradle/
│   └── wrapper/              <-- Gradle 버전 관리 및 자동 설치용
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew                   <-- 유닉스용 Gradle 실행 스크립트
├── gradlew.bat               <-- 윈도우용 Gradle 실행 스크립트

 

2. build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.0'    // 스프링 부트 플러그인
    id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.example'      // 그룹 ID (패키지명처럼 사용)
version = '0.0.1-SNAPSHOT' // 버전

java {
    sourceCompatibility = '17'  // 자바 버전 설정
}

repositories {
    mavenCentral()  // 라이브러리 다운로드 위치
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'  // 웹 기능
    testImplementation 'org.springframework.boot:spring-boot-starter-test' // 테스트 기능
}

 

 

3. Lombok

Lombok은 자바 개발에서 반복적으로 작성해야 하는 코드를 자동으로 생성해주는 코드 간소화 라이브러리입니다. 예를 들어, 클래스에서 자주 사용하는 getter, setter, toString, equals, hashCode, 생성자 등을 직접 작성하지 않아도 @Getter, @Setter, @ToString, @AllArgsConstructor, @NoArgsConstructor 같은 Lombok 어노테이션만 붙이면 컴파일 시점에 자동으로 생성해줍니다. 이로 인해 코드가 훨씬 깔끔해지고, 유지보수도 쉬워지며, 특히 DTO나 Entity 클래스 작성 시 매우 유용하게 사용됩니다. 단, IDE에서 Lombok 플러그인을 설치해야 정상 동작하며, 빌드 도구(Gradle/Maven)에도 의존성을 추가해야 합니다.

 

1. Gradle (또는 Maven)에 Lombok 의존성 추가

추가 후에는 Gradle Reload (또는 Refresh 버튼) 꼭 눌러주세요.

dependencies {
    implementation 'org.projectlombok:lombok:1.18.30'
    annotationProcessor 'org.projectlombok:lombok:1.18.30'
}

 

2. IntelliJ에서 Lombok 플러그인 설치

  1. IntelliJ 메뉴에서 File → Settings(환경설정) 클릭
    (Mac은 IntelliJ IDEA → Preferences)
  2. 왼쪽 메뉴에서 Plugins 선택
  3. 오른쪽 위 검색창에 Lombok 입력
  4. "Lombok Plugin" 설치
  5. 설치 후 IntelliJ 재시작

 

3. 설정 확인 (Lombok 기능 활성화 여부)

체크하지 않으면 Lombok 어노테이션이 적용되지 않습니다!

  1. File → Settings → Build, Execution, Deployment → Compiler → Annotation Processors
  2. Enable annotation processing 체크
// src/main/java/com/example/demo/User.java
package com.example.demo;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class User {
    private String name;
    private int age;
}

 

 

4. JUnit

JUnit은 자바에서 가장 널리 사용되는 단위 테스트 프레임워크로, 메서드 단위로 코드를 테스트하고 검증할 수 있도록 도와주는 도구입니다. 개발자는 @Test 어노테이션을 통해 특정 메서드의 동작을 검증하고, assertEquals, assertTrue 같은 다양한 검증 메서드를 사용해 기대한 결과와 실제 결과를 비교할 수 있습니다. JUnit은 코드의 신뢰성과 유지보수성을 높여주며, 특히 TDD(테스트 주도 개발)나 CI/CD 환경에서 자동화된 테스트를 실행하는 데 필수적인 도구로 활용됩니다. 최근에는 JUnit 5가 주로 사용되며, 이전 버전보다 더 유연하고 확장 가능한 구조를 가지고 있습니다.

myapp/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/demo/
│   │   │       └── User.java          <-- 💡 실제 클래스
│   └── test/
│       └── java/
│           └── com/example/demo/
│               └── UserTest.java      <-- ✅ 테스트 클래스

 

UserTest.java

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class UserTest {

    @Test
    void testUserGetterSetter() {
        User user = new User();
        user.setName("김사과");
        user.setAge(25);

        assertEquals("김사과", user.getName());
        assertEquals(25, user.getAge());
    }
}

1. 테스트 실행 방법

  • 클래스 이름 우클릭 → Run 'UserTest'
  • 또는 함수 위에 있는 녹색 ▶ 버튼 클릭
  • 성공 시 녹색 체크 ✅, 실패 시 빨간 X ❌

 

2. IntelliJ가 테스트 프레임워크를 인식하지 못할 때

  1. File → Project Structure → Modules → Dependencies 탭
  2. JUnit이 있는지 확인 (없으면 수동 추가)
  3. 또는 File → Invalidate Caches / Restart... 시도

https://spring.io/tools

 

Spring | Tools

 

spring.io

 

5. 스프링부트의 View 선택

1. JSP

Spring Boot에서도 JSP 사용은 가능하지만, 공식적으로 권장되지 않으며 Spring Boot의 내장 톰캣(embedded Tomcat)과는 일부 호환 문제가 있어서 다음과 같은 제약이 있습니다.

  • JSP는 /WEB-INF/views/*.jsp 같은 WAR 전용 구조에 더 적합
  • Jar 패키징에서는 JSP가 정상 작동하지 않음
  • 따라서 Spring Boot + JSP = war 패키징 + 외부 톰캣 배포가 일반적

 

디렉토리 구조

src/
 ├─ main/
 │   ├─ java/
 │   │   └─ com.example.demo/
 │   │       └─ controller/
 │   │           └─ HomeController.java
 │   └─ webapp/
 │       └─ WEB-INF/
 │           └─ views/
 │               └─ home.jsp

 

build.gradle (또는 pom.xml)

plugins {
    id 'java'
    id 'war'
    id 'org.springframework.boot' version '3.5.0'
    id 'io.spring.dependency-management' version '1.1.7'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    compileOnly 'org.apache.tomcat.embed:tomcat-embed-jasper'
    runtimeOnly 'org.apache.tomcat.embed:tomcat-embed-jasper'
    implementation 'javax.servlet:jstl:1.2'
}

 

HomeController.java

@Controller
public class HomeController {
    @GetMapping("/home")
    public String home() {
        return "home"; // /WEB-INF/views/home.jsp 로 포워딩
    }
}

 

home.jsp

<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head><title>Home</title></head>
<body>
    <h1>Hello, JSP from Spring Boot!</h1>
</body>
</html>

 

2. Thymeleaf

기본적으로 Spring Boot는 Thymeleaf, Mustache, Freemarker 같은 템플릿 엔진과 REST API + 프론트엔드 분리 구조를 선호합니다. 타임리프(Thymeleaf)는 HTML을 기반으로 작동하는 자바 서버 사이드 템플릿 엔진으로, Spring Boot와 매우 잘 통합되어 주로 웹 페이지를 동적으로 생성할 때 사용됩니다. 타임리프는 HTML 파일을 그대로 브라우저에서 열어도 깨지지 않는 정상적인 HTML 문서 형태를 유지하면서, ${변수}와 같은 표현식이나 th:if, th:each 같은 속성으로 데이터를 출력하고 조건/반복 처리 등을 할 수 있도록 도와줍니다. JSP보다 설정이 간편하고 HTML 디자이너와 개발자가 협업하기 쉬우며, 템플릿 캐시, 레이아웃 구성, 국제화(i18n) 등도 쉽게 지원하여 실무에서 널리 사용되고 있습니다.

 

build.gradle 설정

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}

 

디렉토리 구조

src/
 ├─ main/
 │   ├─ java/
 │   │   └─ com/example/demo/
 │   │       └─ HomeController.java
 │   └─ resources/
 │       ├─ templates/
 │       │   └─ home.html   ← ✅ 타임리프 템플릿
 │       └─ application.properties

 

HomeController.java

package com.example.demo;

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

@Controller
public class HomeController {

    @GetMapping("/home")
    public String home(Model model) {
        model.addAttribute("name", "김사과");
        return "home"; // → templates/home.html 렌더링
    }
}

 

home.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>타임리프 예제</title>
</head>
<body>
    <h1 th:text="'안녕하세요, ' + ${name} + '님!'">안녕하세요, 손님!</h1>
</body>
</html>

'Server > String Boot' 카테고리의 다른 글

JPA  (0) 2025.06.30
RESTAPI  (0) 2025.06.19
Thymeleaf  (0) 2025.06.18

** MyBatis

  • Java 애플리케이션에서 SQL을 XML 또는 어노테이션으로 관리하면서 SQL 결과를 자바 객체에 매핑해주는 ORM 프레임 워크, 자바에서 자주 사용되는 프레임워크

* 동작 원리

  1. SqlSessionFactory 생성
  2. SqlSession 열기
  3. Mapper에서 SQL 실행
  4. 결과를 자바 객체로 매핑
  5. SqlSession 닫기

https://mybatis.org/mybatis-3/

 

MyBatis 3 | Introduction – mybatis

What is MyBatis? MyBatis is a first class persistence framework with support for custom SQL, stored procedures and advanced mappings. MyBatis eliminates almost all of the JDBC code and manual setting of parameters and retrieval of results. MyBatis can use

mybatis.org

 

'Server > Servlet' 카테고리의 다른 글

RESTAPI_frontend  (0) 2025.06.19
MVC2  (0) 2025.06.12
Tomcat9  (3) 2025.06.10
Servlet  (0) 2025.06.09
Session & Servlet api  (0) 2025.06.05

+ Recent posts