보통 이 ERROR는 LOGS 폴더에 대한 권한 문제이거나 메모리 사용량 문제일 경우도 있는데 나는 그 두 케이스에 해당되지 않았다.
테스트를 위해 짧은 간격으로 스케줄을 걸어 확인했을 때는 정상 작동하는 경우도 있고 작동하지 않고 LOGS 하위 폴더에 log 파일도 생성하지 않는 경우도 있다.
Airflow UI 내 log
*** Could not read served logs: Request URL is missing an 'http://' or 'https://' protocol.
Airflow_Webserver.log
[2024-03-27 09:33:32 +0900] [3098307] [WARNING] Invalid request from ip=192.168.21.145: [SSL: SSLV3_ALERT_CERTIFICATE_UNKNOWN] sslv3 alert certificate unknown (_ssl.c:2633)
[[34m2024-03-27T09:33:32.142+0900[0m] {[34mfile_task_handler.py:[0m568} ERROR[0m - Could not read served logs[0m
Traceback (most recent call last):
File "/app/airflow/httpx/_transports/default.py", line 60, in map_httpcore_exceptions
yield
File "/app/airflow/httpx/_transports/default.py", line 218, in handle_request
resp = self._pool.handle_request(req)
File "/app/airflow/httpcore/_sync/connection_pool.py", line 208, in handle_request
raise UnsupportedProtocol(
httpcore.UnsupportedProtocol: Request URL is missing an 'http://' or 'https://' protocol.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/app/airflow/airflow/utils/log/file_task_handler.py", line 546, in _read_from_logs_server
response = _fetch_logs_from_service(url, rel_path)
File "/app/airflow/airflow/utils/log/file_task_handler.py", line 92, in _fetch_logs_from_service
response = httpx.get(
File "/app/airflow/httpx/_api.py", line 189, in get
return request(
File "/app/airflow/httpx/_api.py", line 100, in request
return client.request(
File "/app/airflow/httpx/_client.py", line 821, in request
return self.send(request, auth=auth, follow_redirects=follow_redirects)
File "/app/airflow/httpx/_client.py", line 908, in send
response = self._send_handling_auth(
File "/app/airflow/httpx/_client.py", line 936, in _send_handling_auth
response = self._send_handling_redirects(
File "/app/airflow/httpx/_client.py", line 973, in _send_handling_redirects
response = self._send_single_request(request)
File "/app/airflow/httpx/_client.py", line 1009, in _send_single_request
response = transport.handle_request(request)
File "/app/airflow/httpx/_transports/default.py", line 218, in handle_request
resp = self._pool.handle_request(req)
File "/usr/lib64/python3.9/contextlib.py", line 137, in __exit__
self.gen.throw(typ, value, traceback)
File "/app/airflow/httpx/_transports/default.py", line 77, in map_httpcore_exceptions
raise mapped_exc(message) from exc
httpx.UnsupportedProtocol: Request URL is missing an 'http://' or 'https://' protocol.
개발 환경은 서버 1개, DB 1개라 문제가 없는데
지금 문제는 프로덕션 환경에서 서버 2개, DB 1개일 때에 해당된다.
서버 1번 ) webaserver 및 scheduler 기동 중 DAG 생성 완료
서버 2번 ) 서버 1과 같은 세팅 완료. DAG만 없음 ( webaserver 및 scheduler 기동 중 )
AIRFLOW는 서버가 다르더라도 DB 메타 데이터 기준으로 WORKER들을 생성한다.
1번 서버에 5분 간격으로 실행하는 DAG를 업로드하였으나
2번에는 DAG는 없이 WORKER만 보내진 격이니 에러를 뱉어낼 수 밖에!
생각도 못했던 2번 서버 LOGS 하위 폴더에 log 파일이 생성되어 있었다.
[2024-03-27T13:02:00.691+0900] {scheduler_job_runner.py:781} ERROR -
Executor reports task instance <TaskInstance: scheduled__2024-03-27T04:00:00+00:00
[queued]> finished (failed) although the task says it's queued. (Info: None) Was the task killed externally?
2번 서버 스케줄러 중단하니 말끔하게 처리ㅠ
혹은 2번 서버에도 DAG를 넣어주면 된다.
요약) 서버는 달라도 DB 기준으로 실제 WORK가 작동하고 DAG는 각각의 서버에서 관리된다.
# Whether to load the DAG examples that ship with Airflow. It's good to
# get started, but you probably want to set this to ``False`` in a production
# environment
#
# Variable: AIRFLOW__CORE__LOAD_EXAMPLES
#
#load_examples = True
load_examples = False
처음 세팅할 때 load_examples = False로 세팅
예시 사용 후 삭제할 때는?
airflow.cfg에 load_examples = False로 세팅 후 데이터베이스에 남아있는 이력들을 지워야 한다.
net.sf.log4jdbc.sql.jdbcapi.DriverSpy와 org.postgresql.Driver는 서로 다른 JDBC 드라이버입니다. JDBC 드라이버는 특정 데이터베이스와의 연결을 지원하는 데 사용되며, 각각의 드라이버는 특정 데이터베이스 제품과의 상호 작용을 처리하는 역할을 합니다.
net.sf.log4jdbc.sql.jdbcapi.DriverSpy:
log4jdbc는 JDBC 드라이버를 래핑하여 SQL 쿼리를 로깅하고 분석하는 데 사용되는 라이브러리입니다.
DriverSpy는 이 라이브러리에서 제공하는 JDBC 드라이버 중 하나로, 실제 데이터베이스 연결은 내부적으로 다른 JDBC 드라이버에 위임하면서 SQL 로깅을 추가로 수행합니다.
이 드라이버를 사용하면 애플리케이션에서 실행되는 모든 SQL 쿼리를 로그에 남길 수 있습니다.
org.postgresql.Driver:
PostgreSQL 데이터베이스에 연결하기 위한 JDBC 드라이버입니다.
PostgreSQL 데이터베이스와 상호 작용할 때는 주로 이 드라이버를 사용합니다.
org.postgresql.Driver를 사용하면 데이터베이스 연결 및 SQL 쿼리 수행이 기본적으로 이 드라이버에 의해 처리됩니다.
따라서 선택하는 드라이버에 따라 다음과 같은 차이가 있을 수 있습니다:
net.sf.log4jdbc.sql.jdbcapi.DriverSpy를 사용하면 SQL 쿼리가 추가적으로 로깅되어 개발 및 디버깅에 도움이 될 수 있습니다. 하지만 성능에는 약간의 오버헤드가 발생할 수 있습니다.
org.postgresql.Driver를 사용하면 PostgreSQL 데이터베이스와 직접 상호 작용할 수 있습니다. SQL 로깅은 추가로 수행되지 않습니다. 일반적으로 운영 환경에서는 이러한 SQL 로깅을 최소화하는 것이 권장됩니다.
어떤 드라이버를 선택할지는 프로젝트의 요구사항과 개발 및 디버깅 목적에 따라 다를 수 있습니다. 개발 환경에서는 net.sf.log4jdbc.sql.jdbcapi.DriverSpy를 사용하여 SQL 로깅을 편리하게 수행할 수 있으며, 운영 환경에서는 필요에 따라 선택할 수 있습니다.
@Data
public class Item {
@NotNull(groups = UpdateCheck.class) //수정시에만 적용
private Long id;
}
- Controller
@PostMapping("/{id}/edit")
public String editV2(@PathVariable Long id, @Validated(UpdateCheck.class)
@ModelAttribute Item item, BindingResult bindingResult) {
//...
}
2. 폼 전송을 위한 별도 모델 객체 생성
각 생성, 수정 시 주고 받는 데이터들이 다음과 같다고 치자.
Field
생성
수정
id
X
O
name
O
O
age
O
O
Controller가 있는 경로에 form 패키지를 추가하여 생성, 수정 폼 모델 객체를 새로 만들었다.
- Domain
@Data
public class ItemSaveForm {
@NotBlank
private String name;
@NotNull
@Range(min = 1, max = 150)
private Integer age;
}
@Data
public class ItemUpdateForm {
@NotNull
private long id;
@NotBlank
private String name;
@NotNull
private Integer age; // 수정 시에는 범위 검증X
}
- Controller
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult,
RedirectAttributes redirectAttributes, Model model) {
log.info("bindingResult.getObjectName() => {}",bindingResult.getObjectName()); // item
log.info("bindingResult.getTarget() => {}", bindingResult.getTarget()); // toString() override
// domain에서 @ScriptAssert으로 검증할 수 있으나 자바 코드 권장
// object 에러는 자바 코드로, field error는 Bean Validation으로 진행 권장
// 특정 필드가 아닌 복합 룰 검증 (method로 뽑아 쓰는거 권장)
// 여기가 복합 룰 검증 코드
// 검증에 실패하면 다시 입력 폼으로
if(bindingResult.hasErrors()){
log.info("bindingResult = {}", bindingResult);
// model에 자동으로 담아줌
return "test/addForm";
}
// 성공 로직
Item item = new Item();
item.setName(form.getName()); // 원래는 GETTER, SETTER 없이 생성자로 하는 것이 좋다.
item.setAge(form.setAge());
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/test/{id}";
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {
// 특정 필드가 아닌 복합 룰 검증 (method로 뽑아 쓰는거 권장)
// 여기가 복합 룰 검증 코드
// 검증에 실패하면 다시 입력 폼으로
if(bindingResult.hasErrors()){
log.info("bindingResult = {}", bindingResult);
// model에 자동으로 담아줌
return "test/editForm";
}
// 성공 로직
Item itemParam = new Item();
item.setName(form.getName()); // 원래는 GETTER, SETTER 없이 생성자로 하는 것이 좋다.
item.setAge(form.setAge());
itemRepository.update(itemId, itemParam);
return "redirect:/test/{id}";
}
* 복합 룰 검증 코드는 설계에 따라 다르므로 주석 처리함.
>> 생성 시 코드만 따로 보자
1) @ModelAttribute("item") ItemSaveForm form
Item 객체가 아닌 ItemSaveForm 객체에 담아야한다.
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult,
RedirectAttributes redirectAttributes, Model model) {
//...
}
이럴 경우 Item 객체로 변환하는 과정이 필요하다. (이런 과정이 복잡하게 느껴서 폼을 하나로 하면 그러다 더 망할 확률이 높다.)
2) 폼 객체를 Item 객체로 변환
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult,
RedirectAttributes redirectAttributes, Model model) {
// 검증에 실패하면 다시 입력 폼으로
if(bindingResult.hasErrors()){
log.info("bindingResult = {}", bindingResult);
return "test/addForm";
}
// 성공 로직
Item item = new Item();
item.setName(form.getName()); // 원래는 GETTER, SETTER 없이 생성자로 하는 것이 좋다.
item.setAge(form.setAge());
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/test/{id}";
}
Item 객체 생성해서
Form객체로 부터 생성자 OR getter, setter로 넣어준 Item 객체 값을 Repositroy에 보내야한다!
🔩 HTTP 메시지 컨버터
@Valid, @Validated 적용 가능
- Controller (Domain은 동일)
@PostMapping("/add")
public Object addItem(@RequestBody @Validated ItemSaveForm form, BindingResult bindingResult){
// ItemSaveForm 객체가 만들어지지 않으면 controller 호출 x
log.info("api controller 호출");
if(bindingResult.hasErrors()){
log.info("검증 오류 발생 errors={}", bindingResult);
return bindingResult.getAllErrors(); // 실무에서는 필요한 것만 뽑아서 객체 만들어서 반환 필요
}
log.info("성공 로직 실행");
return form;
}
1. 타입 오류 : 400 에러
예시) age 필드에 "Merry" 라는 String 값을 넣으면
ItemSaveForm객체 자체가 만들어지지 않아 Bean Validation은 적용이 되지 않는다. Json 객체가 만들어지지 않았으니 당연히 Controller 호출도 되지 않는다. (전체 객체 단위로 적용)
처음 공부를 하고 게시판을 만들었을 때 입력 폼과, 수정 폼 비슷한데 한번에 쓰면 되지 않나? 라는 생각을 했던 적이 있었다. 그런 단계에서 이 강의를 봤으면 말끔하게 생각 정리를 할 수 있었을 것 같다.
Validaion 챕터는 세부 단계로 나눠서 따라가다 보니 긴가민가했었는데 Bean Validation에서는 확실히 이해가 쉬웠다. ModelAttribute는 각각 필드 단위로 이루어지고 RequestBody에서는 Json 객체가 만들어지지 않으면 Controller를 태우지 않고 검증 단계로 이어가지 않는다는 정리까지도 좋았다. 생각보다 많은 어노테이션으로 검증을 할 수 있었다. 여기 실무 프로젝트에서는 직접 Validator를 작성해서 검증하는 것으로 보이는데 한번 조사를 해봐야겠다!
>> 내가 담당하고 있는 프로젝트에는 없어서 동료 프로젝트 validator를 간략하게 확인해봤는데