스프링 부트3

장수빈·2026년 7월 15일

1. Spring Boot initialisation

문제점 1. Build 안보임

강사님의 화면에선 왼쪽 하단에 Build 망치 아이콘이 떴었는데 내 화면에서는 뜨지 않았다.
따라서 나는 오른쪽 코끼리 아이콘을 눌러 Gradle을 따로 설치해주었다.

문제점 2. spring boot 설치 이상

gradle 망치 아이콘과 파일 기본 구조가 강사님의 화면과 달라서 이상함을 느끼긴 했었는데 어떻게 내가 바꾸니까 비슷하게 되길래 괜찮은줄 알았는데 아니었다...
애초에 처음 압축을 풀고 open을 할 때 잘못 넣은 것이었다.

바깥쪽 파일이 아닌 gradle이 설치된 바로 전 파일을 열어야 할 것 같다.

Gradle?

프로젝트를 빌드하고 필요한 라이브러리를 관리해 주는 도구.
인텔리제이는 개발 환경이고, Gradle은 그 안에서 프로젝트 실행에 필요한 작업을 담당한다.

2. 패키지별 용도

bulid.Gradle

: Gradle에게 프로젝트를 어떻게 관리할지 알려주는 설정 파일(스프링 부트 버전, 자바 버전, 의존성)

main.java(스프링 프로젝트용)

: 자바 클래스를 자바 코드로만 쓸 수 있는 패키지

main.resources(스프링 프로젝트용)

: 자바 이외의 정적 자료, html css, 이미지 등 자바의 외적인 것을 저장하는 폴더

test(테스트 코드용)

: java와 resources를 테스트 볼 수 있는 코드

3. 스프링부트 아키텍처


각 Controller, Service와 같은 패키지로 이동할 때는 DTO라는 데이터를 이동시키는 클래스를 사용하여야 한다.
그러나 Repository에서 DB로 데이터를 이동시킬 때는 Entity를 사용해야 한다.
-> Entity 필드명이 DB의 테이블과 하나씩 대응되기 때문

왜 이렇게 단을 나누어야 하나?


위처럼 하는 일을 명확히 구분하고, 코드의 재사용성을 높이기 위해서

4. 패키지 작성방법

DDD(도메인 주도 설계): 비슷한 일을 하는 domain(영역)끼리 패키지로 묶고 그 안에 Repository, Service, Controller 등 클래스를 만든다.

ex)

왜 Controller 패키지는 domain 안에 넣지 않고 뺴둘까?

개수가 적고 가장 먼저 사용자에게 요청을 받을 것이기 때문에 관습적으로 앞으로 빼둔다.

1) Config

: 프로젝트 전체에 걸쳐 특정한 설정을 할당해줄때 사용하는 패키지(DB 설정, Spring Security 등)

다만 자바 클래스 작성은 매우 번거롭기 때문에 스프링 부트에서는 간단하게 변수 설정으로 해당 기능을 지원하고 있다.

변수 설정 경로: main -> src -> resources -> applicatinon.properties

config와 application.properties

config - 이 객체를 스프링 빈으로 등록해라, 보안 요청은 이런 규칙으로 처리해라, CORS는 이런 경로에서 허용해라
-> 스프링이 어떻게 동작할지 코드로 정의
application.properties - DB 주소는 어디인지, 서버 포트는 몇 번인지, JPA를 어떤 방식으로 동작시킬지
-> 환경에 따라 바뀔 수 있는 설정값을 저장

2) application.properties

MySQL DB 연결

// RDB 중 연결할 DB 엔진에 대한 드라이브 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
// DB가 구동되는 IP (주소)
spring.datasource.url=jdbc:mysql://아이피:포트/디비명?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true
// 접속 아이디/비밀번호
spring.datasource.username=root
spring.datasource.password=root

JPA 설정

// 자바 클래스 기반으로 MySQL 테이블 자동 생성 여부 (update : 스프링 기반으로 MySQL 테이블 업데이트)
spring.jpa.hibernate.ddl-auto=update

엔티티를 기반으로 MtSQL 테이블이 자동으로 생성(create) 또는 화면처럼 업데이트(update) 될 수 있도록 할 수 있다. 자리에 create, none, update 등 무엇을 넣느냐에 따라 달라질 수 있다.
기본적으로 개발 환경에서는 update, 배포 환경에서는 none을 사용한다.

포트설정

// 스프링 Entity와 MySQL 테이블 필드간 필드명 작명 전략
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

3) util

: 전체적인 스프링 프로젝트에서 사용될 메소드들을 클래스로 만들어서 저장해두는 패키지

5. Security 추가

1) 인증

: 호텔 프론트에서 특정사람에게 키를 부여

2) 인가

: 호텔 각각의 서비스(방, 라운지, 커뮤니티)에서 접근하는 사람을 검증 -> 인증이 되었다고 하더라도 특정 인가가 필요한 서비스에는 접근하지 못할수도 있음

ex) 게시글 작성 프로그램에서도 로그인 후 글을 쓰는 것은 가능하지만(인증만 필요한 서비스) 글을 삭제하거나 수정하기 위해서는 인가가 필요하다.(글의 작성자 또는 관리인)

-> 이러한 인증/인가를 처음부터 구현하는 것은 어렵기 때문에 'spring security'라는 의존성을 활용

의존성 추가하는 방법



스프링 이니셜라이저 -> add -> 의존성 추가 -> explore -> dependencies에 starter와 starter-test 복사 -> 인텔리제이 build.gradle dependencies 쪽에 붙여넣기 -> 코끼리

6. 프로젝트에 시큐리티 설정

application.properties로도 설정이 가능하지만 앞서 말한 차이점 때문에 config 클래스 작성 필요

@Configuration
@EnableWebSecurity
public class Securityconfig {


    //스프링 시큐리티에서는 비밀번호를 암호화해서 저장해야 함. 이때 사용할 암호화 클래스
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    // 시큐리티 설정
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        // csrf 보안 해제
        http
                .csrf(csrf -> csrf.disable());

        // 접근 경로별 인가 설정
        http
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/**").permitAll());

        // 로그인 방식 설정 Form 로그인 방식
        http
                .formLogin(Customizer.withDefaults());

        return http.build();
    }
}

1) 어노테이션

@Configuration - 이 클래스가 Spring 설정 클래스라는 뜻
@EnableWebSecurity - 스프링 시큐리티의 웹 보안 설정 활성화, 요청이 들어보면 security filter를 거쳐 인증 인가를 검사하도록 한다.
@Bean(BCryptPasssordEncoder) - BCryptPasssordEncoder 객체를 Spring Bean으로 등록하는 코드
회원가입 시 비밀번호를 그대로 DB에 저장하면 안되기 때문에 암호화 작업을 거친다.

2) 접근 경로별 인가 설정

permitAll()로 현재 모든 경로에 로그인 없이 접근할 수 있다.

csrf란?

: 로그인한 사용자를 속여서 원하지 않는 요청을 보내게 만드는 공격
따라서 spring security는 CSRF Token를 발급하고 Session Cookie와 CSRF Token 모두 맞아야 요청을 처리한다. CSRF 토큰이 없으면 403 Forbidden을 반환하기 때문에 이 프로젝트에서는 기능을 사용하지 않는다.

Form 로그인 방식

: Spring Security가 기본으로 제공하는 (세션)로그인 방식(로그인 화면도 Spring이 만들어 주고, 로그인 처리도 Spring이 해준다)

7. User 도메인 작성

블로그 서비스에서 요구사항

  • 유저별 게시글 CRUD
  • 유저별 인증/인가 (로그인)

Create: 회원가입
Read: 로그인 / 회원정보 읽기
Update: 회원정보 수정 / 회원탈퇴
Delete: 회원탈퇴

1) 테이블 생성 - Entity

@Getter
@Setter
@Entity
public class UserEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    private String password;

    private String nickname;
}

@Id + @GeneratedValue - PK 번호 + 할당 전략
private '데이터타입' '변수명' - 데이터베이스 필드와 일대일 매핑

(@Setter는 실무인 경우, 쓰는 것을 지양하는 것이 좋지만 간단한 구현을 위해 사용하겠다.)

@Setter를 잘 안쓰는 이유

: 객체의 값을 아무 곳에서나 변경할 수 있어서 객체의 일관성이 깨질 수 있기 때문

2) User 계급화

수직적 계급 vs 수평적 계급

수직적 계급: 여러 유저를 class 별로 관리하기 위함 -> 메소드에 접근하는 유저의 role 체크를 통해 구현(시큐리티 설정)
수평적 계급: 동일한 계급 내부에서 각각의 유저를 관리하기 위함(같은 user 계급이더라도 자신의 정보와 자신의 글만 삭제 가능) -> 메소드에 접근하려는 username과 현재 로그인한 유저의 username을 비교(직접 구현)

아래는 UserRoleType(enum) 코드내용이다.

@Getter
public enum UserRoleType {

    ADMIN("아드민"),
    USER("유저");

    private final String discription;

    UserRoleType(String discription) {
        this.discription = discription;
    }
}

미리 정해진 값만을 사용할 수 있는 enum의 자료형에 따라 enum의 상수, 즉 사용자 권한을 admin과 user로 제한했다.

아래는 UserRoleType 클래스가 추가됨에 따라 추가된 UserEntity 코드이다.

@Enumerated(EnumType.STRING)
    private UserRoleType role;

엔티티의 UserRoleType 필드에 enum 값을 저장하지만 DB에는 문자열로 저장하겠다.
-> DB에 저장되는 값 = ADMIN, USER
-> description 필드의 값 = '아드민', '유저'

왜 enum값을 문자열로 저장할까?

@Enumerated은 순서 번호로 데이터를 저장한다.
이때 값이 추가되거나 순서가 바뀌면 문제가 생길 수 있으므로 문자열로 전환하여 저장한다.
steps할 때 리스트로 받고 문자열로 저장했다가 다시 리스트로 반환하도록 프로그램을 잘못만든 경험이 있어 이것도 프로젝트를 간단하게 만들기 위한 임시적인 건가 했는데 아니엇당

Role과 enum

Role - 인증/인가를 쉽게 구현하도록 도와주는 '스프링 시큐리티'는 role이라는 속성을 가지고 수직적인 계급을 관리한다.
enum - role의 경우 String 형태의 필드보다는 미리 계급을 정해서 enum화 시켜주는 것이 좋다.

Role 계층 추가

서비스에 대해 수직적 계급을 만들었지만 스프링 시큐리티 프레임워크는 우리가 만든 enum 계급을 알지 못함
-> SecurityConfig 에 @Bean 메소드를 등록하여 우리의 계급 체계를 알려주어야 한다.
(admin이 user보다 권한이 넓다는 것을 인식시키는 것)

아래는 추가된 SercurityConfig 코드내용이다.

    // 시큐리티 role 수직적 계층 적용
    @Bean
    public RoleHierarchy roleHierarchy() {

        return RoleHierarchyImpl.withRolePrefix("ROLE_")
                .role(UserRoleType.ADMIN.toString()).implies(UserRoleType.USER.toString())
                .build();
    }

시큐리티 role 수직적 계층 적용

admin 권한을 가진 사람은 user 권한도 포함해서 가진 것으로 설정
.role(UserRoleType.ADMIN.toString()): 이 권한이
.implies(UserRoleType.USER.toSpring()): 이 권한을 포함한다.

3) 테이블에 행위를 수행할 메소드 작성 - Service

정보 저장소에 대한 CRUD

repository에서 값을 받아와 요청을 처리해야 한다.(private)
필드변수 선언 + 값 초기화

@Service
public class UserService {

    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;


    public UserService(UserRepository userRepository, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.userRepository = userRepository;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }
}

(1) Create

: UserEntity에 새로운 회원 등록 = 회원가입

// 유저 한명 생성
    @Transactional
    public void createOneUser(UserRequestDTO dto){
        String username = dto.getUsername();
        String password = dto.getPassword();
        String nickname = dto.getNickname();

        // 동일한 username이 있는지 확인
        if (userRepository.existsByUsername(username)){
            return;
        }

        // 유저에 대한 Entity 생성: DTO -> Entity 및 추가정보 set
        UserEntity entity = new UserEntity();
        entity.setUsername(username);
        entity.setPassword(bCryptPasswordEncoder.encode(password));
        entity.setNickname(nickname);
        entity.setRole(UserRoleType.USER);

        // Entity 저장
        userRepository.save(entity);

@Transactional

Serivce 코드를 작성할 때 유용하게 쓰이는 어노테이션
코드가 잘못 실행되거나 오류가 나서 되돌아가게 될 경우 기존 작업이 소실되지 않도록 해준다.

(2) Read

UserEntity에 존재하는 특정 유저 읽기
회원 정보를 응답할 DTO

// 유저 한명 읽기
    @Transactional(readOnly = true)
    public UserResponseDTO readOneUser(String username){

        UserEntity entity = userRepository.findByUsername(username).orElseThrow();

        UserResponseDTO dto = new UserResponseDTO();
        dto.setUsername(entity.getUsername());
        dto.setRole(entity.getRole().toString());

        return dto;
    }
}

orElseThrow

user가 존재하지 않으면 Optional이 빈 상태로 반환될 수 있기 때문에 orElseThrow를 사용하여 값이 없을 경우 예외를 발생시킨다.

UserEntity에 존재하는 모든 유저 읽기

회원 정보를 응답할 DTO

// 유저 모두 읽기
    @Transactional(readOnly = true)
    public List<UserResponseDTO> readAllUsers(){
        List<UserEntity> list = userRepository.findAll();
        
        List<UserResponseDTO> dtos = new ArrayList<>();
        for (UserEntity user : list){
            UserResponseDTO dto = new UserResponseDTO();
            dto.setUsername(user.getUsername());
            dto.setNickname(user.getRole().toString());
            
            dtos.add(dto);
        }
        
        return dtos;

로그인용 read

로그인은 HTML Form 태그로 클라이언트가 보낸 username/password를 Service 계층에서 username 기반으로 유저를 조회하여 비교 검증하는 방식이다. 하지만 스프링 시큐리티를 사용하면 비교검증 파트는 자동으로 생성되기 때문에 userEntity 테이블에서 특정 username 데이터를 읽는 메소드만 구현하면 된다.

@Service
public class UserService implements UserDetailsService {
// 유저 로그인 (로그인 같은 경우 읽기지만 시큐리티 형식으로 맞춰야 함)
    @Transactional(readOnly = true)
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        UserEntity entity = userRepository.findByUsername(username).orElseThrow();

        return User.builder()
                .username(entity.getUsername())
                .password(entity.getPassword())
                .roles(entity.getRole().toString())
                .build();
    }

여기서 사용한 User는 직접 만든 UserEntity가 아니라 Spring Security에서 제공하는 org.springframework.security.core.userdetails.User 클래스이다.

(3) Update

UserEntity에 이미 존재하는 특정 유저의 정보 수정(닉네임, 비밀번호)

  1. 사용자가 수정할 username과 데이터 -> DTO로 보냄
  2. Service에서 username 기반으로 데이터를 read
  3. DTO에서 받은 데이터를 기반으로 기존 데이터에 신규 값을 초기화
  4. 저장
// 유저 한명 수정
    @Transactional
    public void updateOneUser(UserRequestDTO dto, String username){
        
        //  기존 유저 정보 읽기
        UserEntity entity = userRepository.findByUsername(username).orElseThrow();
        
        // 비밀번호가 입력된 경우에만 변경
        if (dto.getPassword() != null && !dto.getPassword().isEmpty()){
            entity.setPassword(bCryptPasswordEncoder.encode(dto.getPassword()));
        }
        
        // 닉네임이 입력된 경우에만 변경
        if (dto.getNickname() != null && !dto.getNickname().isEmpty()){
            entity.setNickname(dto.getNickname());
        }
        
        // 새 정보 저장
        userRepository.save(entity);
    }

stpes 할때에는 프로필카드 수정기능을 만들때 모든 정보를 삭제하고 다시 작성해서 넣도록 했는데 이렇게 비교해서 값이 입력된 것만 수정되도록 하는것이 더 일반적인 방법인 것 같다.

(4) Delete

service

// 유저 정보 삭제
    @Transactional
    public void deleteOneUser(String username ){
        userRepository.deleteByUsername(username);
    }

repository

@Transactional
    void deleteByUsername(String username);

User CRUD 접근 제한

// 유저 접근 권한 체크
    public Boolean isAccess(String username) {

        // 현재 로그인 되어 있는 유저의 username
        String sessionUsername = SecurityContextHolder.getContext().getAuthentication().getName();
        // 현재 로그인 되어 있는 유저의 role
        String sessionRole = SecurityContextHolder.getContext().getAuthentication().getAuthorities().iterator().next().getAuthority();

        // 수직적으로 ADMIN이면 무조건 접근 가능
        if ("ROLE_ADMIN".equals(sessionRole)) {
            return true;
        }

        // 수평적으로 특정 행위를 수행할 username이 세션(현재 로그인한) username과 같은지
        if (username.equals(sessionUsername)) {
            return true;
        }

        // 나머지 불가
        return false;
    }

4) User 도메인 메소드와 다른 도메인 메소드를 조합한 API 작성 - Controller

사용자가 경로에 데이터를 보냈을 때 올바른 메서드가 실행되도록 Controller에 엔드포인트를 만들어야 한다.

API 엔드포인트

  • 회원가입 페이지: GET"/user/join"

  • 회원가입 진행: POST"/user/join"

  • 회원 수정 페이지: GET"/user/update/{username}"

  • 회원 수정 진행: POST"/user/update{username}"

  • 회원 삭제 진행: DELETE"/user/delete/{username}"

  • 로그인 페이지: GET"/login"(스프링 시큐리티 기본제공)

  • 로그인 진행: POST"/login"(스프링 시큐리티 기본제공)

회원가입 controller 엔드포인트 설정

@Controller
public class UserController {

    private final UserService userService;
    
    public UserController(UserService userService){
        this.userService = userService;
    }
    
    // 회원가입: 페이지 응답
    @GetMapping("/join")
    public String joinPage() {
        return "join";
    }

    // 회원가입: 수행
    @PostMapping("/join")
    public String joinProcess(UserRequestDTO dto) {
        userService.createOneUser(dto);
        return "redirect:/join";
    }
}

회원 수정 controller 엔드포인트 설정

// 회원 수정 : 페이지 응답
    @GetMapping("/user/update/{username}")
    public String updatePage(@PathVariable("username") String username, Model model) {

        // 본인 또는 ADMIN 권한만 접근 가능
        if (userService.isAccess(username)) {
            UserResponseDTO dto = userService.readOneUser(username);
            model.addAttribute("USER", dto);
            return "update";
        }

		// 접근 권한이 없으면 로그인 페이지로 반환
        return "redirect:/login";
    }

    // 회원 수정 : 수행
    @PostMapping("/user/update/{username}")
    public String updateProcess(@PathVariable String username, UserRequestDTO dto) {

        // 본인 또는 ADMIN 권한만 접근 가능
        if (userService.isAccess(username)) {
            userService.updateOneUser(dto, username);
        }

		// 수정 완료된 후 다시 회원 수정 페이지
        return "redirect:/user/update/" + username;
    }

URL의 {username} 부분에는 수정할 회원의 아이디가 들어간다.
@PathVariable은 URL 경로에 포함된 값을 메서드의 매개변수로 받아오는 역할을 한다.

8. Board 도메인 작성

1) 테이블 생성 Entity

BoardEntity

@Entity
@Getter
@Setter
public class BoardEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    @Column(columnDefinition = "TEXT")
    private String content;
}

@Id를 사용하여 Id를 PK로 지정하고 @GeneratedValue로 이를 자동생성하게 한다.
content의 경우 VARCHAR로 받으면 받을 수 있는 글의 한도가 너무 적기 때문에 TEXT를 사용하여 받는다.

2) User와 JOIN

User와 Board 간의 상관관계를 만든다.

User 하나당 여러개의 게시글을 가질 수 있고 게시글 하나당 한명의 유저와 연결된다. -> 일대다관계

BoardEntity 추가코드

@ManyToOne
    private UserEntity userEntity;

여러 개의 게시글이 하나의 사용자와 연결될 수 있으므로 BoardEntity에는 @ManyToOne을 사용한다.
BoardEntity 테이블에 사용자 PK를 참조하는 외래 키가 생성된다.

UserEntity 추가코드

@OneToMany(mappedBy = "userEntity", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<BoardEntity> boardEntityList = new ArrayList<>();

    // 유저에 대해 새로운 글을 추가할 때 : 추가할 글을 받아서 연관관계에 매핑해줌
    public void addBoardEntity(BoardEntity entity) {
        entity.setUserEntity(this);
        boardEntityList.add(entity);
    }

    // 유저에 대해 기존 글을 삭제할 때 : 삭제할 글을 받아서 연관관계에서 뺌
    public void removeBoardEntity(BoardEntity entity) {
        entity.setUserEntity(null);
        boardEntityList.remove(entity);
    }

한 명의 사용자가 여러 개의 게시글을 가질 수 있으므로 UserEntity에는 @OneToMany를 사용한다.
게시글이 여러 개 존재할 수 있기 때문에 List< BoardEntity > 형태로 관리한다.

cascade = CascadeType.ALL
사용자 엔티티에 수행한 영속성 작업을 해당 사용자의 게시글에도 함께 적용 -> 사용자를 삭제하면 연결된 게시글도 함께 삭제될 수 있다.

CascadeType.ALL에는 저장, 수정, 삭제 등의 전파 기능이 모두 포함된다.

addBoardEntity()와 removeBoardEntity()는 연관관계 양쪽의 값을 함께 변경하여 객체 상태를 일관되게 유지하기 위한 연관관계 편의 메서드이다.

3) 테이블에 행위를 수행할 메소드 작성 Service

CRUD 작성

(1) Create

게시글 생성 DTO

@Getter
@Setter
public class BoardRequestDTO {

    private String title;
    private String content;
}

Service

// 게시글 생성
    @Transactional
    public void createOneBoard(BoardRequestDTO dto) {

        // 게시글 dto -> entity
        BoardEntity boardEntity = new BoardEntity();
        boardEntity.setTitle(dto.getTitle());
        boardEntity.setContent(dto.getContent());

        // entity 저장
        boardRepository.save(boardEntity);

        // 현재 게시글을 작성하는 유저
        String username = SecurityContextHolder.getContext().getAuthentication().getName();

        // 해당 유저의 Entity 가져오기
        UserEntity userEntity = userRepository.findByUsername(username).orElseThrow();

        // 연관 관계 만드는 메소드 호출
        userEntity.addBoardEntity(boardEntity);
        userRepository.save(userEntity);
    }

게시글 작성 후 등록과 동시에 UserEntity와 연관관계가 생긴다.

(2) Read

BoardEntity에 존재하는 특정 글 읽기
게시글 읽기 DTO

@Getter
@Setter
public class BoardResponseDTO {

    private Long id;
    private String title;
    private String content;
}

Service

// 게시글 하나 읽기
    @Transactional(readOnly = true)
    public BoardResponseDTO readOneBoard(Long id) {

        BoardEntity boardEntity = boardRepository.findById(id).orElseThrow();

        BoardResponseDTO dto = new BoardResponseDTO();
        dto.setId(boardEntity.getId());
        dto.setTitle(boardEntity.getTitle());
        dto.setContent(boardEntity.getContent());

        return dto;
    }

BoardEntity에 존재하는 모든 게시글 읽기

// 게시글 모두 읽기
    @Transactional(readOnly = true)
    public List<BoardResponseDTO> readAllBoards() {

        List<BoardEntity> list = boardRepository.findAll();

        List<BoardResponseDTO> dtos = new ArrayList<>();
        for (BoardEntity boardEntity : list) {
            BoardResponseDTO dto = new BoardResponseDTO();
            dto.setId(boardEntity.getId());
            dto.setTitle(boardEntity.getTitle());
            dto.setContent(boardEntity.getContent());

            dtos.add(dto);
        }

        return dtos;
    }

게시글은 모두가 읽을 수 있도록 설정한다.

(3) Update

BoardEntity에 존재하는 특정 게시글 수정
-> 특정 게시글 id를 기반으로 데이터 수정 진행

// 게시글 하나 수정
    @Transactional
    public void updateOneBoard(Long id, BoardRequestDTO dto) {

        // 기존의 id에 대한 게시글 데이터 불러오기
        BoardEntity boardEntity = boardRepository.findById(id).orElseThrow();

        // 게시글 dto -> entity
        boardEntity.setTitle(dto.getTitle());
        boardEntity.setContent(dto.getContent());

		// 수정된 데이터 저장
        boardRepository.save(boardEntity);
    }

게시글 수정 및 삭제의 경우 작성한 유저만 수정, 삭제를 할 수 있어야하기 때문에 접근 권한 확인이 필요

Service 코드 추가

// 유저 접근 권한 체크
    public Boolean isAccess(Long id) {

        // 현재 로그인 되어 있는 유저의 username
        String sessionUsername = SecurityContextHolder.getContext().getAuthentication().getName();
        // 현재 로그인 되어 있는 유저의 role
        String sessionRole = SecurityContextHolder.getContext().getAuthentication().getAuthorities().iterator().next().getAuthority();

        // 수직적으로 ADMIN이면 무조건 접근 가능
        if ("ROLE_ADMIN".equals(sessionRole)) {
            return true;
        }

        // 특정 게시글 id에 대해 본인이 작성 했는지 확인
        String boardUsername = boardRepository.findById(id).orElseThrow().getUserEntity().getUsername();
        if (sessionUsername.equals(boardUsername)) {
            return true;
        }

        // 나머지 불가
        return false;
    }

(4) Delete

BoardEntity에 존재하는 특정 게시글 삭제
-> 특정 id를 기반으로 게시글 데이터를 삭제하는 방법

// 게시글 하나 삭제
@Transactional
public void deleteOneBoard(Long id) {
    
    boardRepository.deleteById(id);
}

4) Board 도메인 메소드와 다른 도메인 메소드를 조합한 API 작성 Controller

  • 글 작성 페이지 : GET“/board/create”
  • 글 작성 진행 : POST“/board/create”
  • 글 목록 페이지 : GET“/board/read”
  • 글 읽기 페이지 : GET“/board/read/{id}”
  • 글 수정 페이지 : GET“/board/update/{id}”
  • 글 수정 진행 : POST“/board/update/{id}”
  • 글 삭제 진행 : POST“/board/delete/{id}”
@Controller
public class BoardController {

    private final BoardService boardService;

    public BoardController(BoardService boardService) {
        this.boardService = boardService;
    }

    // 글 생성 : 페이지 응답
    @GetMapping("/board/create")
    public String createPage() {

        return "createBoard";
    }

    // 글 생성 : 수행
    @PostMapping("/board/create")
    public String createProcess(BoardRequestDTO dto) {

        boardService.createOneBoard(dto);

		// 글 수정 완료 후 수정된 글목록 페이지로 이동
        return "redirect:/board/read";
    }

redirect:은 수정사항을 반영하여 페이지를 보여준다는 뜻

0개의 댓글