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:
ByungCheol
2026-05-30 21:18:35 +09:00
commit 9243a41385
20 changed files with 827 additions and 0 deletions
@@ -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);
}
}