@
Initial commit: SB 백엔드 (Spring Boot + MyBatis) - 사용자 CRUD REST API (/api/users) - MariaDB(MyBatis) + Redis 캐싱 구성 - CORS 설정 및 헬스 체크 엔드포인트 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
package com.example.sb_bt;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.example.sb_bt.mapper")
|
||||
public class SbBtApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SbBtApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.example.sb_bt.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Vue 개발 서버(기본 5173)에서의 호출을 허용하기 위한 CORS 설정.
|
||||
* 운영 환경에서는 allowedOrigins 를 실제 도메인으로 제한하세요.
|
||||
*/
|
||||
@Configuration
|
||||
public class CorsConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/api/**")
|
||||
.allowedOrigins("http://localhost:5173")
|
||||
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.example.sb_bt.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* RedisTemplate 구성.
|
||||
* - Key : String 직렬화
|
||||
* - Value : JSON 직렬화 (객체 저장 가능)
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
StringRedisSerializer stringSerializer = new StringRedisSerializer();
|
||||
GenericJackson2JsonRedisSerializer jsonSerializer =
|
||||
new GenericJackson2JsonRedisSerializer(new ObjectMapper());
|
||||
|
||||
template.setKeySerializer(stringSerializer);
|
||||
template.setHashKeySerializer(stringSerializer);
|
||||
template.setValueSerializer(jsonSerializer);
|
||||
template.setHashValueSerializer(jsonSerializer);
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.example.sb_bt.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class HealthController {
|
||||
|
||||
@GetMapping("/health")
|
||||
public Map<String, Object> health() {
|
||||
return Map.of(
|
||||
"status", "UP",
|
||||
"service", "sb_bt"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.example.sb_bt.controller;
|
||||
|
||||
import com.example.sb_bt.domain.User;
|
||||
import com.example.sb_bt.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
@RequiredArgsConstructor
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
@GetMapping
|
||||
public List<User> list() {
|
||||
return userService.findAll();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<User> get(@PathVariable Long id) {
|
||||
User user = userService.findById(id);
|
||||
return user == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(user);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<User> create(@RequestBody User user) {
|
||||
User created = userService.create(user);
|
||||
return ResponseEntity.created(URI.create("/api/users/" + created.getId())).body(created);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<User> update(@PathVariable Long id, @RequestBody User user) {
|
||||
return ResponseEntity.ok(userService.update(id, user));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||
userService.delete(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.example.sb_bt.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 샘플 도메인. users 테이블과 매핑.
|
||||
* Redis 캐싱(JSON 직렬화)을 위해 Serializable 구현.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class User implements Serializable {
|
||||
|
||||
private Long id;
|
||||
private String name;
|
||||
private String email;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.sb_bt.mapper;
|
||||
|
||||
import com.example.sb_bt.domain.User;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MyBatis 매퍼. SQL 은 resources/mapper/UserMapper.xml 에 정의.
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserMapper {
|
||||
|
||||
List<User> findAll();
|
||||
|
||||
User findById(@Param("id") Long id);
|
||||
|
||||
int insert(User user);
|
||||
|
||||
int update(User user);
|
||||
|
||||
int delete(@Param("id") Long id);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.example.sb_bt.service;
|
||||
|
||||
import com.example.sb_bt.domain.User;
|
||||
import com.example.sb_bt.mapper.UserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 샘플 서비스. 단건 조회 시 Redis 캐시를 사용하는 예시 포함.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
private static final String CACHE_KEY_PREFIX = "user:";
|
||||
private static final Duration CACHE_TTL = Duration.ofMinutes(10);
|
||||
|
||||
public List<User> findAll() {
|
||||
return userMapper.findAll();
|
||||
}
|
||||
|
||||
public User findById(Long id) {
|
||||
String key = CACHE_KEY_PREFIX + id;
|
||||
|
||||
Object cached = redisTemplate.opsForValue().get(key);
|
||||
if (cached instanceof User user) {
|
||||
log.debug("[cache hit] {}", key);
|
||||
return user;
|
||||
}
|
||||
|
||||
User user = userMapper.findById(id);
|
||||
if (user != null) {
|
||||
redisTemplate.opsForValue().set(key, user, CACHE_TTL);
|
||||
log.debug("[cache put] {}", key);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public User create(User user) {
|
||||
userMapper.insert(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public User update(Long id, User user) {
|
||||
user.setId(id);
|
||||
userMapper.update(user);
|
||||
redisTemplate.delete(CACHE_KEY_PREFIX + id);
|
||||
return userMapper.findById(id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long id) {
|
||||
userMapper.delete(id);
|
||||
redisTemplate.delete(CACHE_KEY_PREFIX + id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
server:
|
||||
port: 8080
|
||||
servlet:
|
||||
context-path: /
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: sb_bt
|
||||
|
||||
# ===== MariaDB =====
|
||||
datasource:
|
||||
driver-class-name: org.mariadb.jdbc.Driver
|
||||
url: jdbc:mariadb://localhost:3306/sb_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Seoul
|
||||
username: sb_user
|
||||
password: sb_pass
|
||||
hikari:
|
||||
maximum-pool-size: 10
|
||||
minimum-idle: 2
|
||||
connection-timeout: 30000
|
||||
pool-name: sbHikariPool
|
||||
|
||||
# ===== Redis =====
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password:
|
||||
database: 0
|
||||
timeout: 3000ms
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 8
|
||||
max-idle: 8
|
||||
min-idle: 0
|
||||
|
||||
# ===== MyBatis =====
|
||||
mybatis:
|
||||
type-aliases-package: com.example.sb_bt.domain
|
||||
mapper-locations: classpath:mapper/**/*.xml
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
jdbc-type-for-null: 'NULL'
|
||||
default-fetch-size: 100
|
||||
default-statement-timeout: 30
|
||||
|
||||
# ===== Logging =====
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.example.sb_bt: DEBUG
|
||||
com.example.sb_bt.mapper: DEBUG
|
||||
@@ -0,0 +1,34 @@
|
||||
-- ============================================================
|
||||
-- sb_bt 초기 스키마 / 시드 데이터 (MariaDB)
|
||||
-- 사용법:
|
||||
-- 1) MariaDB 접속 후 아래 DB/계정 생성 (관리자 권한)
|
||||
-- 2) sb_db 선택 후 users 테이블 생성 및 시드 입력
|
||||
-- ============================================================
|
||||
|
||||
-- 데이터베이스 및 계정 (최초 1회, root 등 관리자로 실행)
|
||||
CREATE DATABASE IF NOT EXISTS sb_db
|
||||
DEFAULT CHARACTER SET utf8mb4
|
||||
DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
CREATE USER IF NOT EXISTS 'sb_user'@'%' IDENTIFIED BY 'sb_pass';
|
||||
GRANT ALL PRIVILEGES ON sb_db.* TO 'sb_user'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
USE sb_db;
|
||||
|
||||
-- 샘플 테이블
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_users_email (email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 시드 데이터
|
||||
INSERT INTO users (name, email) VALUES
|
||||
('홍길동', 'hong@example.com'),
|
||||
('김철수', 'kim@example.com'),
|
||||
('이영희', 'lee@example.com')
|
||||
ON DUPLICATE KEY UPDATE name = VALUES(name);
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.example.sb_bt.mapper.UserMapper">
|
||||
|
||||
<resultMap id="userResultMap" type="User">
|
||||
<id property="id" column="id"/>
|
||||
<result property="name" column="name"/>
|
||||
<result property="email" column="email"/>
|
||||
<result property="createdAt" column="created_at"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="findAll" resultMap="userResultMap">
|
||||
SELECT id, name, email, created_at
|
||||
FROM users
|
||||
ORDER BY id DESC
|
||||
</select>
|
||||
|
||||
<select id="findById" parameterType="long" resultMap="userResultMap">
|
||||
SELECT id, name, email, created_at
|
||||
FROM users
|
||||
WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
<insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
|
||||
INSERT INTO users (name, email, created_at)
|
||||
VALUES (#{name}, #{email}, NOW())
|
||||
</insert>
|
||||
|
||||
<update id="update" parameterType="User">
|
||||
UPDATE users
|
||||
SET name = #{name},
|
||||
email = #{email}
|
||||
WHERE id = #{id}
|
||||
</update>
|
||||
|
||||
<delete id="delete" parameterType="long">
|
||||
DELETE FROM users
|
||||
WHERE id = #{id}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.sb_bt;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class SbBtApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user