GENERATE 버튼을 클릭해서 실행 파일이 담긴 zip 파일을 다운로드 후 압축해제후 ide에서 열기
System.out.println("id = " + id);
System.out.println("name = " + name);// src/main/java/blog/HelloWorldApplication.java
package blog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWorldApplication.class, args);
}
}// src/main/java/blog/HelloWorldController.java
package blog;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloWorldController {
@GetMapping("helloWorld")
public String helloWorld(){
return "Hello World26";
}
}
이후 HelloWorldApplication 실행하고 http://localhost:8080/helloWorld 접속.
# src/main/resources/application.properties
server.port=8081
server.servlet.context-path=/hello-world접근 방법: http://localhost:8081/hello-world/helloWorld
# src/main/resources/application.yaml
server:
port: 8082
servlet:
context-path: /hello-world-new접근 방법: http://localhost:8082/hello-world-new/helloWorld
RequestMapping interface로 GET/POST/DELETE/PUT Mapping을 대체 가능.
// src/main/java/blog/TestRestController.java
package blog;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestRestController {
private static String studentName_1 = "tom";
private static String studentName_2 = "kitty";
// @GetMapping("/student")
@RequestMapping(value = "/student", method = RequestMethod.GET)
public String getStudent(@RequestParam String id){
if("1".equals(id)) {
return studentName_1;
}else{
return studentName_2;
}
}
// @PostMapping("/student")
@RequestMapping(value = "/student", method = RequestMethod.POST)
public String addStudent(@RequestParam String id, @RequestParam String name){
if("1".equals(id)) {
studentName_1 = name;
}else{
studentName_2 = name;
}
return "ok";
}
// @PutMapping("/student")
@RequestMapping(value = "/student", method = RequestMethod.PUT)
public String updateStudent(@RequestParam String id, @RequestParam String name){
if("1".equals(id)) {
studentName_1 = name;
}else{
studentName_2 = name;
}
return "ok";
}
// @DeleteMapping("/student")
@RequestMapping(value = "/student", method = RequestMethod.DELETE)
public String deleteStudent(@RequestParam String id){
if("1".equals(id)) {
studentName_1 = null;
}else{
studentName_2 = null;
}
return "ok";
}
}// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
}// src/main/java/blog/SecurityConfig.java
package blog;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
private static final String ROLE = "ADMIN";
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager =
new InMemoryUserDetailsManager();
manager.createUser(
User.withUsername("aaa")
.password(
PasswordEncoderFactories
.createDelegatingPasswordEncoder()
.encode("bbb"))
.authorities(ROLE)
.build());
return manager;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/helloWorld").permitAll()
.requestMatchers("/helloWorld1").hasAuthority(ROLE)
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
}// src/main/java/blog/HelloWorldController.java
package blog;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloWorldController {
@GetMapping("helloWorld")
public String helloWorld(){
return "Hello World26";
}
@PreAuthorize("hasAuthority('ADMIN')")
@GetMapping("/helloWorld1")
public String helloWorld1() {
return "Hello World1!";
}
}
# src/main/resources/application.yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/hello_world?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: root
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
# spring:
# datasource:
# driver-class-name: org.sqlite.JDBC
# url: jdbc:sqlite:/Users/abc/hello-world.db // 절대경로// build.gradle
dependencies {
// 추가
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'org.xerial:sqlite-jdbc:3.50.3.0'
}// src/main/java/blog/StudentDao.java
package blog;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
@Repository
public class StudentDao {
private final JdbcTemplate jdbcTemplate;
public StudentDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<Map<String, Object>> getStudent(String id) {
return jdbcTemplate.queryForList(
"SELECT * FROM student WHERE id = ?",
id
);
}
public void saveStudent(String id, String name) {
jdbcTemplate.update(
"INSERT INTO student(id, name) VALUES(?, ?)",
id,
name
);
}
public void updateStudent(String id, String name) {
jdbcTemplate.update(
"UPDATE student SET name=? WHERE id=?",
name,
id
);
}
public void deleteStudent(String id) {
jdbcTemplate.update(
"DELETE FROM student WHERE id=?",
id
);
}
}
// src/main/java/blog/StudentController.java
package blog;
import blog.StudentDao;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
public class StudentController {
private final StudentDao studentDao;
public StudentController(StudentDao studentDao) {
this.studentDao = studentDao;
}
@GetMapping("/student")
public Object getStudent(@RequestParam String id) {
List<Map<String, Object>> list = studentDao.getStudent(id);
if (list.isEmpty()) {
return null;
}
return list.get(0);
}
@PostMapping("/student")
public String addStudent(
@RequestParam String id,
@RequestParam String name) {
studentDao.saveStudent(id, name);
return "ok";
}
@PutMapping("/student")
public String updateStudent(
@RequestParam String id,
@RequestParam String name) {
studentDao.updateStudent(id, name);
return "ok";
}
@DeleteMapping("/student")
public String deleteStudent(
@RequestParam String id) {
studentDao.deleteStudent(id);
return "ok";
}
}// build.gradle
dependencies {
// 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'
}// src/main/java/blog/dto/ValidationRequest.java
package blog.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public class ValidationRequest {
@Max(value = 50, message = "count는 최대 50입니다.")
@Min(value = 10, message = "count는 최소 10입니다.")
private int count;
@NotNull(message = "age는 null일 수 없습니다.")
private String age;
@NotBlank(message = "name은 공백일 수 없습니다.")
private String name;
@Size(min = 10, max = 20, message = "content는 10~20자여야 합니다.")
private String content;
@NotEmpty(message = "remark는 비어 있을 수 없습니다.")
private String remark;
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}// src/main/java/blog/ValidationController.java
package blog;
import blog.dto.ValidationRequest;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ValidationController {
@PostMapping("/validation")
public String validation(
@Valid ValidationRequest request,
BindingResult result) {
if (result.hasErrors()) {
return result.getFieldError().getDefaultMessage();
}
return "Validation Success";
}
}Spring Boot에서 DB를 사용하는 방법은 크게 3가지가 있습니다.
// SQL을 Java 코드 안에 작성합니다.
jdbcTemplate.queryForList(
"select * from student where id=?",
id
);// SQL은 XML로 분리됩니다.
Student student = studentMapper.selectStudent(id);<select id="selectStudent">
select * from student where id=#{id}
</select>// SQL도 거의 안 씁니다.
studentRepository.findById(id);# application.yaml
# ...
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true// ...
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4'// Student.java
package blog;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Student implements Serializable {
private static final long serialVersionUID = -7748312639957930069L;
private String id;
private String name;
private StudentClass studentClass;
private StudentSchool studentSchool;
private Teacher teacher;
private List<SchoolBag> schoolBags;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class StudentClass {
private static final long serialVersionUID = -5213219387175188213L;
private Long no;
private String name;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class StudentSchool {
private static final long serialVersionUID = 3997395302485226876L;
private Long sno;
private String sname;
}
}
// Teacher.java
package blog;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Teacher {
private static final long serialVersionUID = 4976235843125918808L;
private String id;
private String name;
}
// SchoolBag.java
package blog;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SchoolBag {
private static final long serialVersionUID = -7699349163224691756L;
private String id;
private String name;
}-- student.sql
CREATE TABLE student(
id integer(10) not null,
name varchar(32) not null,
class_no integer,
class_name text,
school_no integer,
school_name varchar(32));
CREATE TABLE school_bag (
ID INTEGER,
NAME VARCHAR,
STUDENT_ID INTEGER
);
CREATE TABLE teacher (
ID INTEGER,
NAME VARCHAR,
STUDENT_ID INTEGER
);
INSERT INTO student (id,name,class_no,class_name,school_no,school_name) VALUES
(99,'小张张99',1,'一年一班',1,'测试学校');
INSERT INTO school_bag (ID,NAME,STUDENT_ID) VALUES
(1,'书包 1',99),
(2,'书包 2',99),
(3,'书包 3',99);
INSERT INTO teacher (ID,NAME,STUDENT_ID) VALUES
(1,'班主任 1',99);// StudentMyBatisController.java
package blog;
import org.springframework.web.bind.annotation.*;
@RestController
public class StudentMyBatisController {
private final StudentNewMapper studentNewMapper;
public StudentMyBatisController(StudentNewMapper studentNewMapper) {
this.studentNewMapper = studentNewMapper;
}
@RequestMapping(value = "/studentXML", method = RequestMethod.GET)
public Object getStudentXML(@RequestParam Long id){
return studentNewMapper.getStudent(id);
}
@RequestMapping(value = "/studentsXML", method = RequestMethod.GET)
public Object getStudentsXML(){
return studentNewMapper.getStudents();
}
@RequestMapping(value = "/studentXML", method = RequestMethod.POST)
public String addStudentXML(Student student){
studentNewMapper.saveStudent(student);
return "ok";
}
@RequestMapping(value = "/studentXML", method = RequestMethod.PUT)
public String updateStudentXML(Student student){
studentNewMapper.updateStudent(student);
return "ok";
}
@RequestMapping(value = "/studentXML", method = RequestMethod.DELETE)
public String deleteStudentXML(String id){
studentNewMapper.deleteStudent(id);
return "ok";
}
}
// StudentNewMapper.java
package blog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface StudentNewMapper {
Student getStudent(@Param("id") Long id);
List<Student> getStudents();
void saveStudent(@Param("student") Student student);
void updateStudent(@Param("student") Student student);
void deleteStudent(@Param("id") String id);
}<!-- src/main/resources/mapper/StudentNewMapper.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="blog.StudentNewMapper">
<resultMap type="blog.Student" id="student">
<result column="id" property="id"/>
<result column="name" property="name"/>
<association property="studentClass" columnPrefix="class_" autoMapping="true"/>
<association property="studentSchool" columnPrefix="school_" autoMapping="true">
<result property="sno" column="no" />
<result property="sname" column="name" />
</association>
<association property="teacher" columnPrefix="teacher_" autoMapping="true">
<result property="id" column="id" />
<result property="name" column="name" />
</association>
<collection property="schoolBags" javaType="java.util.List" ofType="blog.SchoolBag">
<id property="id" column="school_bag_id"/>
<result property="name" column="school_bag_name"/>
</collection>
</resultMap>
<sql id="studentDetail">
select t.id,
t.name,
t.class_no,
t.class_name,
t.school_no,
t.school_name,
s.id AS school_bag_id,
s.name AS school_bag_name,
e.id AS teacher_id,
e.name AS teacher_name
from student t
left join school_bag s on t.id = s.student_id
left join teacher e on t.id = e.student_id
</sql>
<select id="getStudent" parameterType="Long" resultMap="student">
<include refid="studentDetail"></include>
where t.id = #{id}
</select>
<select id="getStudents" resultMap="student">
<include refid="studentDetail"></include>
</select>
<insert id="saveStudent">
insert into student
(id,
name,
class_no,
class_name,
school_no,
school_name)
values
(#{student.id},
#{student.name},
#{student.studentClass.no},
#{student.studentClass.name},
#{student.studentSchool.sno},
#{student.studentSchool.sname})
</insert>
<update id="updateStudent">
update student
set name = #{student.name},
class_no= #{student.studentClass.no},
class_name= #{student.studentClass.name},
school_no= #{student.studentSchool.sno},
school_name= #{student.studentSchool.sname}
where id = #{student.id}
</update>
<delete id="deleteStudent">
delete from student where id = #{id}
</delete>
</mapper># application.yaml
spring:
datasource:
driver-class-name: org.sqlite.JDBC
url: jdbc:sqlite:/Users/julong/Documents/be-dev/blog/student.db
jpa:
database-platform: org.hibernate.community.dialect.SQLiteDialect
hibernate:
ddl-auto: update
show-sql: true// StudentJpa.java
package blog.jpa;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "student")
public class StudentJpa {
@Id
@Column(name = "id")
private String id;
@Column(name = "name")
private String name;
public StudentJpa() {
}
public StudentJpa(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// StudentJpaController.java
package blog.jpa;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
@RestController
public class StudentJpaController {
@Autowired
private StudentJpaRepository studentJpaRepository;
@GetMapping("/studentJpa")
public Object getStudent(@RequestParam String id){
Optional<StudentJpa> opt = studentJpaRepository.findById(id);
if(opt.isPresent()){
return opt.get();
}
return null;
}
@PostMapping("/studentJpa")
public String addStudent(@RequestParam String id, @RequestParam String name){
studentJpaRepository.save(new StudentJpa(id,name));
return "ok";
}
@PutMapping("/studentJpa")
public String updateStudent(@RequestParam String id, @RequestParam String name){
studentJpaRepository.save(new StudentJpa(id,name));
return "ok";
}
@DeleteMapping("/studentJpa")
public String deleteStudent(@RequestParam String id){
studentJpaRepository.deleteById(id);
return "ok";
}
}
// StudentJpaRepository.java
package blog.jpa;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface StudentJpaRepository extends JpaRepository<StudentJpa, String> {
}
// RestTemplateConfig.java
package blog.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
// RandomUserService.java
package blog.service;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class RandomUserService {
private final RestTemplate restTemplate;
public RandomUserService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public String getRandomUser(int results){
String url =
"https://randomuser.me/api/?results={results}";
return restTemplate.getForObject(
url,
String.class,
results
);
}
}
// UserController.java
package blog.service;
import blog.service.RandomUserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
private final RandomUserService randomUserService;
public UserController(RandomUserService randomUserService) {
this.randomUserService = randomUserService;
}
@GetMapping("/random-user")
public String randomUser(
@RequestParam(defaultValue = "1") int results){
return randomUserService.getRandomUser(results);
}
}// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'javax.cache:cache-api'
implementation 'org.ehcache:ehcache::jakarta'spring:
cache:
jcache:
config: classpath:ehcache.xml<?xml version="1.0" encoding="UTF-8"?>
<config
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns='http://www.ehcache.org/v3'
xmlns:jsr107='http://www.ehcache.org/v3/jsr107'
xsi:schemaLocation="
http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.10.xsd
http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.10.xsd">
<cache alias="randomUser">
<key-type>java.lang.Integer</key-type>
<value-type>java.lang.String</value-type>
<expiry>
<ttl unit="minutes">10</ttl>
</expiry>
<resources>
<heap unit="entries">100</heap>
</resources>
</cache>
</config>// HelloWorldApplication.java <= intro
package blog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching; // <= 추가됨
@EnableCaching // <= 추가됨
@SpringBootApplication
public class HelloWorldApplication {
public static void main(String[] args) {
SpringApplication.run(HelloWorldApplication.class, args);
}
}
// RandomUserService.java
package blog.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class RandomUserService {
private static final Logger log = LoggerFactory.getLogger(RandomUserService.class);
private final RestTemplate restTemplate;
public RandomUserService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
// results 값이 캐시에 있으면 외부 API를 호출하지 않고 캐시된 응답을 반환
@Cacheable(cacheNames = "randomUser", key = "#results")
public String getRandomUser(int results){
// 이 로그가 찍히면 캐시 미스 = 실제 외부 API 호출
log.info("CACHE MISS - randomuser.me 호출 (results={})", results);
String url =
"https://randomuser.me/api/?results={results}";
return restTemplate.getForObject(
url,
String.class,
results
);
}
}test.properties.value:
a: 나는a
b: 나는b
a1: 나는a1
b1: 나는b1
test-a: 나는test-a// EnvConfig.java
package blog.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "test.properties.value")
public class EnvConfig {
private String a1;
private String b1;
private String testA;
public String getA1() {
return a1;
}
public void setA1(String a1) {
this.a1 = a1;
}
public String getB1() {
return b1;
}
public void setB1(String b1) {
this.b1 = b1;
}
public String getTestA() {
return testA;
}
public void setTestA(String testA) {
this.testA = testA;
}
}
// GetEnvConfigController.java
package blog;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import blog.config.EnvConfig;
@Slf4j
@RestController
public class GetEnvConfigController {
@Autowired
private EnvConfig envConfig;
@Value("${test.properties.value.a}")
private String a;
@Value("${test.properties.value.b}")
private String b;
@GetMapping("/envConfig")
public String getEnvConfig(){
log.info("a1={};b1={};test-a={}", envConfig.getA1(),
envConfig.getB1(), envConfig.getTestA());
return "a1=" + envConfig.getA1() + ";b1="
+ envConfig.getB1() + ";test-a="
+ envConfig.getTestA();
}
}<!-- src/main/resources/application.yaml-->
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="false" scanPeriod="60 seconds" debug="false">
<property name="LOG_HOME" value="logs"/>
<property name="appName" value="blog"/>
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] [%level] %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<maxHistory>365</maxHistory>
<maxFileSize>100MB</maxFileSize>
</rollingPolicy>
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] [%level] %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<logger name="blog" level="debug"/>
<root level="info">
<appender-ref ref="stdout"/>
<appender-ref ref="appLogAppender"/>
</root>
</configuration>// TestLogbackController.java
package blog;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class TestLogbackController {
@GetMapping("/logback")
public String logback(){
log.info("info 레벨 - 중요한 정보 출력, 가장 많이 사용");
log.debug("debug 레벨 - 디버깅용, 보통 실무에서 최저 레벨로 사용");
log.error("error 레벨 - 에러 로그 기록, 자주 사용");
log.warn("warn 레벨 - 경고 로그 기록, 자주 사용");
log.trace("trace 레벨 - 실행 흐름 추적용, 거의 사용하지 않음");
int param1 = 1;
String param2 = "param2";
// {} placeholder로 파라미터를 치환해 여러 값을 함께 기록할 수 있다
log.info("전달 파라미터: {}, 두 번째 파라미터: {}", param1, param2);
return "ok";
}
}
| 순번 | 로그 레벨 | 설명 |
|---|---|---|
| 1 | trace | 추적. 프로그램 실행 흐름을 나타낸다. |
| 2 | debug | 디버그. 실무에서는 보통 최저 레벨로 사용하며, trace는 거의 쓰지 않는다. |
| 3 | info | 중요한 정보 출력. 많이 사용한다. |
| 4 | warn | 경고. 많이 사용한다. |
| 5 | error | 에러 정보. 많이 사용한다. |
| 순번 | 출력 포맷 | 설명 |
|---|---|---|
| 1 | %d{yyyy-MM-dd HH:mm:ss, SSS} | 로그 생성 시간. 밀리초까지 출력 |
| 2 | %level | 로그 레벨 출력 |
| 3 | %logger 또는 %c | logger 이름. 보통 패키지명 + 클래스명 |
| 4 | %thread 또는 %t | 현재 스레드 이름 출력 |
| 5 | %p | 로그 레벨(우선순위) 출력. %level과 동일 |
| 6 | %message 또는 %msg 또는 %m | 로그 내용. 예: logger.info("message") |
| 7 | %n | 줄바꿈 문자 |
| 8 | %class 또는 %C | Java 클래스명 출력 |
| 9 | %file 또는 %F | 파일명 출력 |
| 10 | %L | 로그 발생 행 번호 출력 |
| 11 | %method 또는 %M | 메서드명 출력 |
| 12 | %l | 구문이 위치한 행 정보 출력(클래스명, 메서드명, 파일명, 행 번호 포함) |
| 13 | hostName | 로컬 머신 이름 |
| 14 | hostAddress | 로컬 IP 주소 |
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-aop'// AspectController.java
package blog.aspect;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class AspectController {
@GetMapping("/aspect/error")
public String error() throws Exception {
throw new RuntimeException("error");
}
@GetMapping("/aspect/test")
public String test() throws Exception {
log.info("test 메서드 진입");
return "ok";
}
}
// ControllerLogAspect.java
package blog.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class ControllerLogAspect {
// execution 표현식 구조: execution(반환타입 패키지.클래스.메서드(파라미터))
// - 첫 번째 * : 반환 타입 (any)
// - *Controller : Controller로 끝나는 모든 클래스
// - .* : 모든 메서드
// - (..) : 파라미터 개수/타입 무관
// - || 로 여러 패키지를 함께 지정할 수 있다
@Pointcut("execution(* blog.aspect.*Controller.*(..))")
private void logPointcut() {
// 포인트컷 정의용 메서드라 구현이 필요 없다
}
// 메서드 실행 전에 가로챈다
@Before("logPointcut()")
public void beforeRequest(JoinPoint jp) {
log.info("요청 전 (beforeRequest) - {}", jp.getSignature().getName());
}
// 메서드 실행 후, 실제 Response가 나가기 전에 가로챈다
@After("logPointcut()")
public void afterResponse(JoinPoint jp) {
log.info("요청 후, 실제 Response 반환 전 (afterResponse) - {}", jp.getSignature().getName());
}
// 예외를 잡는다. throwing에 지정한 타입과 실제 throw된 예외 타입이 일치해야 한다
@AfterThrowing(pointcut = "logPointcut()", throwing = "ex")
public void logException(RuntimeException ex) {
log.info("RuntimeException: {}", ex.getMessage());
}
// 반환 결과를 잡는다. returning에 지정한 타입과 실제 반환 타입이 일치해야 한다
@AfterReturning(pointcut = "logPointcut()", returning = "result")
public void logResult(String result) {
log.info("result: {}", result);
}
// 실행 전/후, 예외 처리, 반환값 제어를 한 곳에서 모두 할 수 있다
@Around("logPointcut()")
public Object retry(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("요청 전 (retry)");
try {
Object response = proceedingJoinPoint.proceed();
log.info("요청 후, 실제 Response 반환 전 (retry)");
return response;
} catch (Exception e) {
log.info("요청 후 예외 발생, null 반환하도록 처리 (retry)");
return null;
}
}
}