ํด๋น ๊ธ์ Jenkins์ Github Webhook์ ์ด์ฉํ CICD ํ์ดํ๋ผ์ธ ๊ตฌ์ฑํ๊ธฐ ์๋ฆฌ์ฆ ์ ๋๋ค. ์์ธํ ์ฌํญ์ ์๋ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์!
๋ง์ฝ ํด๋น ์ค์ต ๋ด์ฉ์ ์ฝ๋๊ฐ ๊ถ๊ธํ๋ค๋ฉด ํ๋ก์ ํธ ๊นํ๋ธ ์์ ํ์ธํ ์ ์์ต๋๋ค.
- 1ํธ ์๋ฆฌ์ฆ๋ฅผ ์์ํ๋ฉฐ :: ์ปจํ ์ธ ๊ฐ์์ ๋๊ธฐ
- 2ํธ ํ๋ก ํธ์๋ ๊ฐ๋ฐํ๊ธฐ :: ๋ฆฌ์กํธ์ axios
- 3ํธ ๋ฐฑ์๋ ๊ฐ๋ฐํ๊ธฐ :: SpringBoot ์ h2
- 4ํธ ec2 ์์ฑ ๋ฐ Jenkins ์ค์น :: AWS EC2๋ก Jenkins ์๋ฒ ๋ง๋ค๊ธฐ
- 5ํธ Dockerizing ๋ฐ Nginx ์ค์ :: ๋ฐฐํฌ๋ฅผ ์ํ ๋์ปค ๋น๋์ Nginx์ ๋ฆฌ๋ฒ์ค ํ๋ก์
- 6ํธ ์นํ ์ค์ ํ๊ธฐ :: Github Webhook ์ฐ๋ํ๊ธฐ
- 7ํธ pipeline์ผ๋ก ๋ฐฐํฌํ๊ธฐ :: Jenkins Pipeline Script ์์ฑํ๊ธฐ
์์
- ํ๋ก์ ํธ ์์ฑ ๋ฐ ์ธํ
- Model ๊ฐ๋ฐ
- Service ๊ฐ๋ฐ
- Controller ๊ฐ๋ฐ
- Filter ๊ฐ๋ฐ
- ์ด๊ธฐ data๋ฅผ ์ํ import.sql ์์ฑ
- ํ๋ก ํธ์ ๋ฐฑ E2E ์ฐ๊ฒฐํ๊ธฐ
๋ฐฑ์๋ ๊ฐ๋ฐํ๊ธฐ
์ค๋์ CICD ํ์ดํ๋ผ์ธ์ ์ํด์ ํ์ํ ๋ฐฑ์๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฐ๋ฐํด๋ณด๋ ค ํ๋ค.
๋ฐฑ์๋๋ ๋ง์ฐฌ๊ฐ์ง๋ก ์๋ฆฌ์ฆ ์์์ ์ธ๊ธํ์๋ฏ Spring Boot ๋ฅผ ์ด์ฉํ ๊ฒ์ด๋ค.
Spring Boot ์์๋ ๋ค์๊ณผ ๊ฐ์ ์์กด์ฑ๋ค์ ์ฌ์ฉํ ๊ฒ์ด๋ ๋ฏธ๋ฆฌ ํด๋น ์์กด์ฑ์ ๋ณด๊ณ ์ด๋ค ํ๋ฆ์ด๊ฒ ๊ตฌ๋~ ๋ฅผ ์๊ฐํด๋ณด์.
- Spring boot Web
- Spring Data JPA
- H2 Databse
- Lombok
- java ์์ฐ์ฑ์ ์ํ ์์กด์ฑ
- ModelMapper
- Dto to Entity ๋ณํ์ ์ํ ๋งคํผ
1. ํ๋ก์ ํธ ์ ์ฑ ๋ฐ ์ธํ ํ๊ธฐ
ํ๋ก์ ํธ ์์ฑ์ ์ํด์ ์ด์ ์ Frontend ๋ฅผ ๊ฐ๋ฐํ Git ๋๋ ํ ๋ฆฌ๋ก ๊ฐ์ backend ๋ผ๋ ๋๋ ํ ๋ฆฌ๋ฅผ ์์ฑํ๋ค.
๊ทธ๋ฆฌ๊ณ Spring Initializer ๋ฅผ ์ด์ฉํ์ฌ Backend ๋ผ๋ Spring Boot Project๋ฅผ ์์ฑํ๋ค.
๋๋ Intellij ๋ฅผ ์ด์ฉํ๊ธฐ ๋๋ฌธ์ IDE GUI๋ฅผ ์ด์ฉํด์ ์ธํ ํ์ง๋ง Intellij ์์ด vscode ๋ ์ดํด๋ฆฝ์ค๋ฅผ ์ฌ์ฉํ๋ค๋ฉด Spring Initializer ์์ ์ง์ zip ํ์ผ๋ก ๋ด๋ ค๋ฐ์ ์ ์๋ค.
๋์ถฉ ๋ค์๊ณผ ๊ฐ์ ์์กด์ฑ๋ง ๊ฐ์ง๊ฒ ํ๋ฉด๋๋ค.
์ด๋ค ๊ณผ์ ์ ๊ฑฐ์น๋ ์ฐ๋ฆฌ์ Git Repository์ /backend
๋ผ๋ ๋๋ ํ ๋ฆฌ์ ์์น์์ผ์ฃผ๋ฉด ๋๋ค.
์ถ๊ฐ ์์กด์ฑ ModelMapper ์ถ๊ฐํ๊ธฐ ๋ฐ Bean ์ฃผ์
ModelMapper ๋ ๋ํ์ ์ผ๋ก ์์ฃผ ์ฌ์ฉ๋๋ user lib ์ด๋ค.
์ฃผ๋ก ํด๋์ค๊ฐ Converting ๊ธฐ๋ฅ์ ์ํํ๋๋ฐ, ์ฐ๋ฆฌ์ ๊ฒฝ์ฐ์๋ DTO๋ฅผ ์ด์ฉํด์ ๋คํธ์ํฌ ํต์ ๊ฐ์ฒด๋ฅผ ์ ์ํ๋ค.
๋ง์ฝ Spring Converter ์ ์ํด์ ์์ฒญ์ด Json Raw ํ์ ์ด๋ผ๋ฉด ObjectMapper๊ฐ ์ด๋ฅผ DTO๋ก ๋ฐ๊ฟ์ฃผ๋๋ฐ, ์ฌ๊ธฐ๊น์ง๋ Spring์ด ํด์ฃผ๊ณ ์ฐ๋ฆฌ๋ ๋ฐ๊ฟ์ง DTO๋ฅผ ๋ JPA ๊ฐ ์ฌ์ฉํ ์ ์๋๋ก Entity๋ก ์ปจ๋ฒํ ์ ํด์ผํ๋ค.
์ด ๊ณผ์ ์์ ModelMapper ๊ฐ ์ฌ์ฉ๋๋ค.
๊ทธ๋์ ModelMapper๋ฅผ build.gradle
์ ์์กด์ฑ ์ถ๊ฐ๋ฅผ ํด์ฃผ์
dependencies {
implementation 'org.modelmapper:modelmapper:2.1.1'
}
๊ทธ๋ฆฌ๊ณ gradle ํ๋ก์ ํธ๋ฅผ reload ํ ๋ค, ํด๋น ํด๋์ค๋ฅผ Bean ์ผ๋ก ๋ฑ๋ก์์ผ์ผ ํ๋ค.
@SpringBootApplication
public class JenkinsCicdTodoCIApplication {
public static void main(String[] args) {
SpringApplication.run(JenkinsCicdTodoApplication.class, args);
}
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
}
ํด๋น application ์ main ํจ์ ์๋์ Bean ์ผ๋ก ๋ฑ๋ก์ํค๊ธฐ ์ํ ์ฝ๋๋ฅผ ์ถ๊ฐํ์
application.yml ์ผ๋ก ์ธ๋ถ ์ค์ ๊ด๋ฆฌํ๊ธฐ
๋ง์ฝ ํ๋ก์ ํธ๊ฐ ์ ์์ฑ๋์๋ค๋ฉด Spring ์ ์ธ๋ถ ์ค์ ์ ์กฐ์ํด๋ณด์
์ธ๋ถ ์ค์ ์ ๋ณดํต application.properties
ํ์ผ์์ ํ๊ฒ ๋๋ค.
ํ์ง๋ง properties ํ์ผ์ ๊ฐ๋ ์ฑ์ด ์ข์ง ๋ชปํ๊ธฐ ๋๋ฌธ์ ๊ฐ์ธ์ ์ผ๋ก๋ yml ํ์ ์ ์ค์ ํ์ผ์ ์ข์ํ๋ค.
resources
๋๋ ํ ๋ฆฌ ์๋์ ์๋ application.properties
ํ์ผ์ ์ญ์ ํ๊ณ application.yml
ํ์ผ์ ์ถ๊ฐํ์!
๊ทธ๋ฆฌ๊ณ ๋ค์๊ณผ ๊ฐ์ด ๋ช ์ํด์ค๋ค.
spring:
h2:
console:
path: /h2-console
enabled: true
settings:
web-allow-others: true
datasource:
driver-class-name: org.h2.Driver
username: sa
password:
url: jdbc:h2:mem:todo
jpa:
hibernate:
ddl-auto: create-drop
ํด๋น ์๋ฆฌ์ฆ๋ Spring๊ณผ JPA ์ ๋ํ ์ค๋ช ์ด ์๋๋ฏ๋ก ๊ฐ๋ตํ๋ง ๋ณด๊ณ ๋์ด๊ฐ์๋ฉด,
- H2 DB๋ฅผ ์ฌ์ฉํ๋ค.
- ์น์์ H2 DB ์ฝ์์ ์ ๊ทผ์ ํ์ฉํ๋ค
- DB์ ๋ค์ด๊ฐ ๋ฐ์ดํฐ๋ in memory ์์ ๊ด๋ฆฌ๋๋ค.
- ํ์ด๋ฒ๋ค์ดํธ๊ฐ ddl ์ ์ฑ์ ์คํ์ํค๋ฉด create ํ๊ณ ์ฑ์ด ์ข ๋ฃ๋๋ฉด drop ํ๋ค
์ด์ ํ๊ฒฝ ์ค์ ์ด ์๋ฃ๋์์ผ๋ ์ฐ๋ฆฌ์ ์ฑ์ ๊ตฌ๋์ํฌ ์ ์๋ ํด๋์ค๋ค์ ๋ฏธ๋ฆฌ ๋ง๋ค๊ณ ์์ํ์.
๋ค์๊ณผ ๊ฐ์ ํด๋์ค๋ค์ ๋ง๋ค์ด์ฃผ๋ฉด ๋๋ค.
- CorsFilter.java : CORS Filter
- JenkinsCicdTodoApplication.java : Application Entrypoints
- Todo.java : JPA Entity
- TodoController.java : API Endpoints
- TodoRepository.java : Repository
- TodoRequestData.java : DTO
- TodoService.java : Business Layer's Logic
2. Model ๊ฐ๋ฐํ๊ธฐ
Model ์ Layered Architecture ์์ Model Layer ์ ํด๋นํ๋ ์ฝ๋๋ค์ ์๋ฏธํ๋ค.
์ฌ๊ธฐ์ ๊ตฌํํด์ผํ ๊ฒ๋ค์ 3๊ฐ์ด๋ค.
- JPA Entity
- Repository
- DTO
์ฌ์ค์ Repository ๋ JpaRepository ๋ฅผ extendsํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ํฌ๊ฒ ํด์ผํ ๊ฒ๋ค์ ์๋ค.
๋ค์ ์ฝ๋๋ฅผ ์ถ๊ฐํด์ฃผ์.
// Todo.java
@Entity @Setter @Getter
@NoArgsConstructor
@AllArgsConstructor
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
}
// TodoRepository.java
public interface TodoRepository extends JpaRepository<Todo, Long> {
}
// TodoRequestData.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TodoRequestData {
private String content;
}
DTO ์ ํฌํจ๋๋ ๋ฐ์ดํฐ๋ content๋ง ์กด์ฌํ๋ค.
์ด์ฐจํผ Repository ์์ Entity๋ฅผ ์ ์ฅํ ๋, GenerationType ์ IDENTITY๋ก ์ค์ ํด์คฌ๊ธฐ ๋๋ฌธ์ ๋ด๋ถ์ ์ผ๋ก AUTO_INCREMENTS๊ฐ ๋ ๊ฒ์ด๋ค.
3. Service ๊ฐ๋ฐํ๊ธฐ
Service๋ Controller ์์ ๋ค์ด์จ DTO๋ ์์ฒญ์ ๋ฐ๊ณ , Repository๋ก DB๋ก ์กฐํํ๋ ์ญํ ์ ํ๋ค.
์ฆ, Layered Architecture ์์ View Layer์ Model ์ฌ์ด์ ์์นํ๋ layer ๋ก ์ดํดํ๋ฉด ๋ ๊ฒ ๊ฐ๋ค
@Service
@Transactional
public class TodoService {
private final TodoRepository todoRepository;
private final ModelMapper modelMapper;
public TodoService(TodoRepository todoRepository, ModelMapper modelMapper) {
this.todoRepository = todoRepository;
this.modelMapper = modelMapper;
}
/**
* ๋ชจ๋ todo item ์ ๋ฐํํ๋ค.
*
* @return List
*/
public List<Todo> getTodos() {
return todoRepository.findAll();
}
/**
* content ๋ฅผ ๋ฐ๊ณ todo ๋ฅผ ์ ์ฅํ๋ค.
*
* @param todoRequestData
* @return ์์ฑ๋ Todo
*/
public Todo createTodo(TodoRequestData todoRequestData) {
return todoRepository.save(modelMapper.map(todoRequestData, Todo.class));
}
/**
* id ๋ฅผ ๋ฐ๊ณ todo ๋ฅผ ์ญ์ ํ๋ค.
*
* @param todoId todo id
* @return ์ญ์ ๋ todo ์ id
*/
public Long deleteTodo(Long todoId) throws Exception {
Optional<Todo> optionalTodo = todoRepository.findById(todoId);
if(optionalTodo.isEmpty()) {
throw new Exception();
}
todoRepository.delete(optionalTodo.get());
return todoId;
}
}
Controller ์์
์๋๋ผ๋ฉด Entity ๋ก ๋ณํ๋ ๋ฐ์ดํฐ๋ฅผ ๋ DTO๋ก ๋ณํํด์ฃผ๋ ๊ณผ์ ์ด ํ์ํ์ง๋ง ๊ทธ๋ฅ Entity ๋ก ๋ฐํํ๋ค ใ ใ
4. Controller ๊ฐ๋ฐํ๊ธฐ
์ด์ API ์๋ฒ์ Endpoint ์ธ Controller ๋ฅผ ๊ฐ๋ฐํด๋ณด์.
@RestController
@RequestMapping(value = "/api/todos", produces = "application/json")
public class TodoController {
private final TodoService todoService;
public TodoController(TodoService todoService) {
this.todoService = todoService;
}
@GetMapping
public ResponseEntity<List<Todo>> getTodos() {
return ResponseEntity.ok().body(todoService.getTodos());
}
@PostMapping
public ResponseEntity<Todo> createTodo(@RequestBody TodoRequestData todoRequestData) {
return ResponseEntity
.status(HttpStatus.CREATED)
.body(todoService.createTodo(todoRequestData));
}
@DeleteMapping("/{id}")
public ResponseEntity<Long> deleteTodo(@PathVariable Long id) throws Exception {
return ResponseEntity
.status(HttpStatus.ACCEPTED)
.body(todoService.deleteTodo(id));
}
}
๋ชจ๋ API์ Response ๋ก๋ ResponseEntity
๋ฅผ ๋ฐํํ๋๋ก ํ๊ณ ResponseEntity.status
์ ResponseEntity.body
๋ก ๋ฐ์ดํฐ์ ์ํ ๊ฐ์ ๋ฐํํ๋๋ก ํ๋ค.
5. CORS Filter ๊ฐ๋ฐํ๊ธฐ
CORS ๋ ํ๋ก ํธ์๋์ ๋ฐฑ์๋๋ฅผ ๋๋๋ 3-tier ์ํคํ ์ณ์์ ์ค์ํ ๋ณด์ ์์์ด๋ค.
CORS์ ๋ํ ์์ธํ ์ค๋ช ์ ํด๋น ๋ธ๋ก๊ทธ์ OPTIONS ํค๋์ Preflight ๊ทธ๋ฆฌ๊ณ CORS ์์ ํ์ธํ ์ ์์ต๋๋ค.
๋๋ ๋ชจ๋ ์์ฒญ์ ํน์ ๋๋ฉ์ธ๋ง ํ์ฉํ๋ OPTIONS ํค๋๋ฅผ ๋ฐํํ Filter๋ฅผ ๋ง๋ค์ด ์ฃผ์๋ค.
CORS๋ฅผ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์๋ ๋ค์ํ ๋ฐฉ๋ฒ์ด ์๋๋ฐ, CORS ๋ฅผ ํด๊ฒฐํ๋ 3๊ฐ์ง ๋ฐฉ๋ฒ ์์ ํ์ธํ ์ ์๋ค.
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods","*");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Authorization");
if("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
}else {
chain.doFilter(req, res);
}
}
@Override
public void destroy() {
}
}
์๋๋ผ๋ฉด ์ธ๋ถ ์ค์ ํ์ผ๋ก ๋ฐ๋ก ๋นผ์ HOST PC์ IP๋ฅผ ๋ช
์ํด์ผ ํ์ง๋ง ์ผ๋จ์ ๊ฐ๋จํ Jenkins ๊ตฌ์ฑ์ ์ํด์ *
์์ผ๋์นด๋๋ฅผ ์ฌ์ฉํ์ง๋ง ์ด๋ ๋ณด์์ ์ผ๋ก ๋งค์ฐ ์ทจ์ฝํ๋ค๊ณ ํ ์ ์๋ค.
์ด์ ๋ชจ๋ API ๊ฐ๋ฐ์ด ๋๋ฌ๋ค!
๋จ์ ๊ฒ์ ์ ํ๋ฆฌ์ผ์ด์
์ ์คํํ์ ๋, ๊ธฐ๋ณธ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ด์ฃผ๋ import.sql
๋ง ๋ง๋ค๊ณ ํ๋ก ํธ์๋์ ์ฐ๋ํด๋ณด๋๋ก ํ์
6. ์ด๊ธฐ ๋ฐ์ดํฐ๋ฅผ ์ํ import.sql ์์ฑํ๊ธฐ
import.sql ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ด ์คํํ๋ ์์ ์ ํด๋น ํ์ผ์ ์๋ ์ฟผ๋ฆฌ๋ฌธ์ ์คํ์ํจ๋ค.
์ฐ๋ฆฌ๋ JPA์ ddl-auto ์์ฑ์ ํตํด์ ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋์ํ๋ ์์ ์ DDL ๋ฌธ๋ง ์คํํ๋๋ก ํ๋ค.
๊ทธ๋ผ ๊ฒฐ๊ตญ DB ํ ์ด๋ธ์๋ ๋น์ด์๋ ๋ฐ์ดํฐ๋ง ์์ ๊ฒ์ด๊ณ , Front ์์ DB์ ์ ์ฅํ ๋ฐ์ดํฐ๋ฅผ ๋งค๋ฒ ์ถ๊ฐํด์ค์ผ ํ๋ค.
์ด ๊ณผ์ ์ ์์ ๊ณ ์๋ํ ์ํค๊ธฐ ์ํด์ ๋๋ ์ฃผ๋ก import.sql
์ ํ
์คํธ ํ๊ฒฝ์์ ์ด์ฉํ๋ค.
import.sql ์ ์ด์ฉํด์ ์ด๊ธฐ ๋ฐ์ดํฐ๋ฅผ ๋ฃ์ด์ฃผ์
import.sql ์ /resources
๋๋ ํ ๋ฆฌ ์๋์ ์์น์์ผ์ฃผ๋ฉด ๋๋ค.
INSERT INTO todo(content) VALUES ('๋ฆฌ๋์ค ํ์ตํ๊ธฐ');
INSERT INTO todo(content) VALUES ('Greedy ์๊ณ ๋ฆฌ์ฆ 5๋ฌธ์ ํ๊ธฐ');
INSERT INTO todo(content) VALUES ('Jenkins Backend ๊ตฌ์ฑํ๊ธฐ');
INSERT INTO todo(content) VALUES ('๋ชจ๋ํฐ ์ฒญ์');
INSERT INTO todo(content) VALUES ('๋ธ๋ก๊ทธ ํฌ์คํ
์ค๋นํ๊ธฐ');
INSERT INTO todo(content) VALUES ('Effective Java ์ฝ๊ธฐ');
์ด์ ์ง์ง ๋ชจ๋ ์ค๋น๊ฐ ๋๋ฌ๋ค!
ํ๋ก ํธ์ ์ฐ๋ํด๋ณด์.
ํ๋ก ํธ์๋์ E2E ์ฐ๋ํ๊ธฐ
์ฐ๋ฆฌ๋ ์ง๋ ์๊ฐ ํ๋ก ํธ์๋์์ ํ์ํ ๋ฐ์ดํฐ ๋ฐ ์ํ ๋ก์ง์ ๋ชจ๋ ๊ตฌ์ฑํ๊ธฐ ๋๋ฌธ์ ๋ฐฑ์๋ ์๋ฒ์ ํ๋ก ํธ ์๋ฒ๋ฅผ ํค๊ณ ๋จ์ง ์์ฒญ๋ง ๋ณด๋ด๋ฉด ๋๋ค.
์ฐ๋ฆฌ๊ฐ ์์ฑํ ๋ฐฑ์๋์ API ๋ฅผ ํ์ธํด๋ณด์.
- GET
/api/todos
: ํฌ๋ ๋ฆฌ์คํธ์ ๋ชจ๋ ํฌ๋๋ฅผ ์กฐํํ๋ค. - POST
/api/todos
: ํฌ๋ ๋ฆฌ์คํธ์ ํฌ๋๋ฅผ ์ถ๊ฐํ๋ค. - DELETE
/api/todos/{id}
: ํฌ๋ ๋ฆฌ์คํธ์ ํน์ ํฌ๋๋ฅผ ์ญ์ ํ๋ค
ํ๋ก ํธ์๋๊ฐ ์์นํด์๋ /frontend
๋๋ ํ ๋ฆฌ๋ก ๊ฐ์ npm start
๋ช
๋ น์ ์ํํ์
๊ทธ๋ผ ๋ค์๊ณผ ๊ฐ์ด API ์์ฒญ์ด ์คํจํ ๊ฒ์ด๋ค.
์ด๋ ๋น์ฐํ ๋ฐฑ์๋๊ฐ ๋์๊ฐ์ง ์๊ณ ์์ผ๋ ๋ฐ์ํ๋ ๋ฌธ์ ๊ณ /backend
๋๋ ํ ๋ฆฌ๋ก ๊ฐ์ ๋ฐฑ์๋๋ฅผ ์คํ์์ผ์ฃผ์
๊ทธ๋ฆฌ๊ณ ํ๋ก ํธ์๋๋ฅผ ๋ค์ ์๋ก๊ณ ์นจ ํ๋ฉด ๋ค์๊ณผ ๊ฐ์ด Data Fetching ์ด ์ ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
์ด์ CICD ํ์ดํ๋ผ์ธ ๊ตฌ์ฑ์ ์ํ ์ ํ๋ฆฌ์ผ์ด์ ๋ ๋ฒจ์ ์ค๋น๋ ๋ชจ๋ ๋๋ฌ๋ค.
๋ค์ ์๊ฐ์๋ ์ง์ EC2๋ฅผ ๋ง๋ค์ด๋ณด๋ฉฐ Jenkins๋ฅผ ์ค์นํด๋ณด๋๋ก ํ์!
๋๊ธ