
Mybatis에서 enum 활용하기
주차장 관리 솔루션 RESTAPI 를 만들면서, Mybatis를 통해서도 ENUM 을 활용할 방안이 없을까 궁리했다. JPA를 쓸 경우, 어노테이션 처리만으로 간단하게 활용할 수 있었던 것처럼.. 하나만 선언한 ENUM 경우 맴버권한을 나타내는 Enum을 만들었다. public enum Role { USER, ADMIN } BaseTypeHandler 작성 public class MemberAuthorityTypeHandler extends BaseTypeHandler<Role> { @Override public void setNonNullParameter(PreparedStatement ps, int i, Role parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter.name()); } @Override public Role getNullableResult(ResultSet rs, String columnName) throws SQLException { return Role.valueOf(rs.getString(columnName)); } @Override public Role getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return Role.valueOf(rs.getString(columnIndex)); } @Override public Role getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return Role.valueOf(cs.getString(columnIndex)); } } SqlSessionFactory에 등록 @Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource, ApplicationContext applicationContext) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setConfigLocation(applicationContext.getResource("classpath:mybatis/mybatis-config.xml")); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("mybatis/mapper/*.xml")); sqlSessionFactoryBean.setTypeHandlers(new MemberAuthorityTypeHandler()); //등록 return sqlSessionFactoryBean.getObject(); } 등록 Method String rawPassword = member.getMember_password(); String encPassword = bCryptPasswordEncoder.encode(rawPassword); member.commonJoin(encPassword, Role.USER); //회원사 정보 입력 Integer result = adminMapper.memberAdd(member); mybatis xml 설정 <insert id="memberAdd" parameterType="hashmap"> insert into member ( member_userId, member_password, member_name, member_manager, member_phone, member_role, parking_id, member_car_number, member_createdate ) values ( #{member_userId}, #{member_password}, #{member_name}, #{member_manager}, #{member_phone}, #{member_role}, #{parking_id}, #{member_car_number}, now() ) </insert> 인자를 여러개 가진 Enum의 경우 public interface CodeEnum { @JsonValue String getCode(); } public enum Status implements CodeEnum { ACCEPT("승인"), REJECT("반려"), WAIT("신청중"); private String code; Status(String code) { this.code = code; } @MappedTypes(Status.class) public static class TypeHandler extends CodeEnumTypeHandler<Status> { public TypeHandler() { super(Status.class); } } @Override @JsonValue public String getCode() { return code; } } sqlSessionFactoryBean.setTypeHandlers( new MemberAuthorityTypeHandler(), new Status.TypeHandler() ); <update id="updateVisitStatus" > update visiting_schedule as v INNER JOIN parking as p ON v.parking_id = p.id set visit_status = #{visit_status} where v.id = #{id} and p.id=#{parking_id} </update> 참고 https://www.holaxprogramming.com/2015/11/12/spring-boot-mybatis-typehandler/ https://minkwon4.tistory.com/169 https://camelsource.tistory.com/62

자바스크립트 기초 5
JS 객체 소환법 자바스크립트에서 기본타입을 제외한 모든 값은 "객체" 여기서 객체란, ‘이름(key): 값(value)’형태의 프로퍼티를 저장하는 일종의 컨테이너라 보면 됨. 기본타입 객체는 하나의 값만을 갖지만, 참조타입 객체는 여러 개의 값을 가질 수 있슴. 객체의 프로퍼티로 함수가 포함할 수 있으며, 이러한 함수 프로퍼티를 "메서드"라고 함. Object() 생성자 함수 var me = new Object(); me.name = ‘NY KIM’; me.gender = ‘female’; me.age = 25; console.log(me); //결과: { name: ‘NY KIM’, gender: ‘female’, age: 25 } 객체 리터럴 방식 var me = { name : ‘NY KIM’, gender: ‘female’, age: 25 }; 객체에 접근방법 me.name me[‘name’] //프로퍼티가 표현식이거나 예약어인 경우 대괄호 접근법을 써야 합니다! me.full-name //불가. 출력값: NaN me['full-name'] //가능 full(undefined)에서 name(undefiend)을 빼는 연산을 했기 때문이죠. undefined는 존재하지 않는 프로퍼티값에 접근할 때 출력되며, NaN은 수치 연산을 해서 정상적인 값을 얻지 못할 때 출력됩니다. 객체에 접근하여 값을 할당했을 때, 해당 프로퍼티가 있으면 → 값을 갱신하고, 해당 프로퍼티가 없으면 → 값을 할당한다 https://nykim.work/33 undefined 와 null undefined 는 데이터 타입이자 값이다. 아무 것도 존재하지 않는 다는 표현의 값으로 생각하면 좋을 듯하다. 모든 변수에 값이 할당 되지 않는 경우 이 undefined는 값으로써 값이 할당되지 않은 모든 변수들이 자바스크립트 Runtime 시 할당되어 출력되게 된다. undefined는 ‘없음'을 뜻하는 데이터 타입이다. 근데 자바스크립트에도 다른 정적 언어와 마찬가지로 null 이라는 데이터 타입이 존재한다. const n = null; console.log(typeof n); null 도 데이터 타입이기 때문에 null 이라는 출력값을 기대할 수 있겠지만 전혀 예상과 다른 값이 나온다. 이건 명백한 자바스크립트의 오류이다. 그래서 둘의 차이는? undefined은 개발자가 의도적으로 값을 할당할 필요가 없다. 자바스크립트가 코드를 실행할 때 변수에 적절한 값이 할당 되어있지 않거나 아래 함수처럼 return 이 없는 함수를 실행할 때 undefined 를 출력한다. function a() { console.log(123) } 반면 null은 개발자가 명시적으로 변수에 할당한 값 이기 때문에 필요한 곳에서 사용 했구나 라고 인식하면된다. null은 GC 가 쓰지 않는 메모리를 수거할 때 유용하다 https://medium.com/crocusenergy/js-undefined-null-%EC%96%B4%EB%96%A8-%EB%95%8C-%EC%93%B8%EA%B9%8C-8782dc3c35b6

react 설치시 Couldn't find a package.json file
yarn init name : 프로젝트명(기본 폴더명) version : 프로젝트 버전(기본 1.0.0) description : 수행할 작업 내용 entry point : 첫 실행 위치 repository url : 저장소 위치 author : 작업자(프로젝트 팀이라면 팀명) license : 라이센스 여부 private : 공개 여부??? 응답 후 package.json file 생성됨

환자 모니터 연동 웹 프로젝트
구성 POC 프로젝트가 맡겨졌다. 병원 환자 모니터와 연동할 웹개발인데, 컨셉만 증명하는 프로젝트라 간단할 것 같았던 일이 사실은 고생길의 시작이라는 것을 몰랐다. 라즈베리 파이에서 qt를 통해 gateway서버에 쏘면 gateway 는 원격 웹 서버에 그 정보들을 HL7 protocol로 변환하여 전송하다. 웹 서버는 연동하는 프론트 서버에게 그 정보들을 실시간으로 뿌려준다. P.M(라즈베리파이) 기기는 총 16개가 중앙서버(C.S)와 연결될 것이다. 그림만 이렇게 설계하였을 뿐,(이 그림도 내가 만들었다. 사실상 기획서가 없는 상태에서 시작했다..) 그 외에 어떤 설계도 없었다. 그래서 각자 알아서 만드는 과정에서 많은 장애가 있었다. 일단 요구된 기능은 크게 한 6가지 정도로 추려볼 수 있는데, 요구사항 웹 화면에서 환자 검색 기능, QT에서도 환자 검색가능(둘은 별도의 기능) 기기 하나당 하나의 환자정보 일정시간동안 측정데이터를 날리는데, 그 데이터들을 DB에 저장 측정데이터를 웹 그래프 화면에 표시, QT에서 보내는 그래프 파형과 일치해야됨 환자에 대한 파일을 CentralStation Server로 전송업로드 가능 GW가 살았는지 죽었는지 응답가능 세션 시작시 세션 정보를 저장 보고나니 정말 생각보다 별 거 없다고 생각이 들지만, 구현하는 과정이 만만치 않았다.. 만드는 과정에서 협업은 3명이서 하였다. 프론트쪽 서버를 만드는 프론트개발자 한 분, qt쪽을 만드는 개발자, 나는 gw와 cs( 중앙)서버를 만드는 역할을 담당했다. 프론트쪽 개발에 내가 많이 참여하게 되었다. 개발과정 먼저 QT와 통신할 gateWay 서버를 만들기로 했다. (springboot에 java11 환경) 이 소프트웨어는 qt와 함께 라즈베리파이에 심고, qt와 통신하며 그 결과값을 중앙 centralStation 서버로 쏴주는 역할을 하는 gateWay로서 통신과정에서 TCP layer에서만 빠른 속도로 실시간으로 바이트 단위 전달을 하기 위해 rest 통신 같은 것을 제거하고 소켓으로 통신한다. 그래서 내장톰캣을 껐다. gateWay는 nio socket으로 central station과 qt에 접속한다. 일단 nio socket을 써본다는 것 자체가 곤혹이었다. nio socket은 커녕 일반 socket 관련 프로그램도 학부생 시절에 한 번 만들어 본 게 다이다.. 더군다나 qt 쪽에서 보내는 Data들을 신뢰할 수 없는 상황이니 일일히 예외 테스트 케이스를 만들어서 버퍼로 읽어줘야 됐다. 생각한 것보다 더 로우레벨에 가까운 일이다. 비동기 쪽 로직도 골치상황이었다. QT와 연동할 socket과 CS와 연동할 socket 2개를 동시에 열어줘야했기 때문에, 모든 흐름은 비동기처리를 고려해야했다. 그나마 예전에 swing으로 game을 만들면서 multi-thread에 대한 개념이 있어서 다행이지.. 아니었다면 많이 헤멨을 상황이었다. 아무튼 고생고생하며 QT에서 보내는 데이터를 하나의 제이슨 단위로 읽어서 처리하는 과정을 완료했다. 여기까지는 좋았다. 헷갈리긴 하였지만, 크게 어려운 부분도 없었고, 첫번째 난항은 QT에서 전송해준 jsonData를 다시 HL7 protocol로 변환해서 C.S 서버에게 전달해주는 과정이었다. 생전 처음 보는 프로토콜이었다. 병원 프로토콜이라는데, 이 프로토콜을 익히는데만 하더라도, 꽤나 고생했다. 대충 아래 같이 변환해줘야 된다. HAPI Protocol example 처음 설계됐던 대로 C.S와 GW은 HL7 Protocol을 사용해서 통신해야 한다. 그냥 편하게 Json으로 할 수 없나요? 물어보니 규격을 따라야 된다고 대답하더라.. 까라면 까라지 정신으로 변환하였다. socket으로 변환된 hl7 protocol을 사용해서, c.s 서버에 전달하는 흐름까지 완성한 후.. c.s 만들기에 착수했다. c.s 서버를 만드는 것도 골칫덩어리다. 일단 붙어있는 DB는 2개여야 되는데, 하나는 사내 DB로 mysql를 쓸 거고, 다른 하나는 EMR로 병원 DB 가 연결해있어야 된다. 각자 테스트 용으로 oracle 스키마와 mysql 스키마를 만들고, ERD를 만들었다. oracle 관련 로직은 실제 병원에서는 프로시저를 호출할 거라, 실제와 비슷한 환경으로 프로시저를 만들어서 호출하는 식으로 했다. 사실 이 상황에서 JPA를 쓰고 싶었지만, 회사에서는 Mybatis를 권유했다. 프론트 개발은 Next.js를 활용하여 개발하였다. 사실은 지금 프로젝트가 서버사이드랜더링을 할 필요가 없어서, 딱히 Next를 쓸 필요 없이 순수한 React.js를 활용하여도 됐지만, 그냥 다른 회사의 프론트 작업물들이 Next.js 위에 굴러갔기 때문에 비슷하게 갈려고 선택했다. 프론트에서 측정데이터를 받는 과정에서는 처음에 웹소켓을 고려했다가, SSE Protocol를 사용하기로 결정했다. 측정데이터를 제외한 Rest 요청같은 경우 axios + redux saga+ redux를 통해서 해결하기로 했다. 개발 난항 HL7 protocol로 변환시 실제로 전송해야되는 정보 중에서 HL7 protocol에 포함되지 않는 정보들도 있다. 그럴때면 임시방편으로 규격을 내가 만들어서 추가해줘야 했다. hl7 protocol를 이해하는 데만 하더라도 꽤나 골머리를 앓았고, 이렇게 원래 규격에 포함이 되지 않을 때는 구색맞추기 식으로 진행하였다. 새삼 JSON이 얼마나 편한 통신규격인지 깨달을 수 있는 과정이었다.. 서버를 여러개 띄워두고 테스트를 해야되니, 정신이 없었다. 기본적으로 띄워야 되는 서버만 하더라도, GW, C.S, FrontEnd Server 그리고 QT를 여기서 내가 돌릴 수 없으니, SocketTest라는 애뮬레이터 프로그램을 다운받아서 대체했다. 4개를 띄워놓고 혼자서 테스트를 하는데, 테스트하는데 얘를 먹었다. DB를 여러개 연결하는 과정도 시간이 걸렸다. 이전에는 springBoot rest API를 만들면서, DB를 하나만 써봤는데, DB를 2개 이상 연결하려니, DB 객체를 Bean으로 등록하는 과정에서 중복이슈가 터졌다. Bean을 만드는 과정에서 싱글톤으로 등록하는데, 여러개의 SqlSessionFactoryBean을 등록하려다 보니, bean의 이름이 어노테이션만 쓰면 자동으로 소문자로 시작해서 등록해주는데, 이름 충돌이 걸린 것이다. 처음에는, BeanNameGenerator 를 상속받아서 이름 짓기 전략을 패키지 명 풀경로까지 지정할려고 했는데, 더 번거로울 것 같아서.. @Primary & @Qualifier 를 활용했다. @Primary & @Qualifier 를 사용하는데 있어서 @RequiredArgsConstructor 가 안 먹히는 이슈는 lombok.config 파일을 만드는 것으로 해결했다. lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier JDBC 로그 남기는 라이브러리를 연결하는데도 애를 먹었다. 일반적인 설정으로는 연결이 안 되더라.. 아래와 같이 maven 의존성을 설정함으로써 해결했다.. <!-- log4jdbc --> <dependency> <groupId>org.bgee.log4jdbc-log4j2</groupId> <artifactId>log4jdbc-log4j2-jdbc4.1</artifactId> <version>1.16</version> </dependency> <dependency> <groupId>com.oracle.ojdbc</groupId> <artifactId>orai18n</artifactId> <version>19.3.0.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- MariaDB는 다른 식으로 연결.. --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <!-- 스프링 부트에서 Log4j2를 사용하기위해선 내부로깅에서 쓰이는 의존성을 제외해주어야 합니다. 기본적으로 Spring은 Slf4j라는 로깅 프레임워크를 사용합니다. 구현체를 손쉽게 교체할 수 있도록 도와주는 프레임 워크입니다. Slf4j는 인터페이스고 내부 구현체로 logback을 가지고 있는데, Log4j2를 사용하기 위해 exclude 해야 합니다. --> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.oracle.database.jdbc</groupId> <artifactId>ojdbc8</artifactId> <scope>runtime</scope> </dependency> 단위 테스트를 하는데도 문제가 생겼는데, 기존의 test 라이브러리가 안 먹혀서 결국 통합 테스트로 대체했다. <dependency> <groupId>com.oracle.database.jdbc</groupId> <artifactId>ojdbc8</artifactId> <scope>runtime</scope> </dependency> //안 먹힘 //@Slf4j //@ExtendWith(SpringExtension.class) // @Transactional //실제 DB에 값이 들어가지 않도록. //@SpringBootTest //public class IntegTest { // // @Qualifier("MysqlPatientMapper") // @Autowired // private PatientMapper patientMapper; // // @Qualifier("MysqlMeasureDataMapper") // @Autowired // private MeasureDataMapper measureDataMapper; // // // @Qualifier("OracleMeasureDataMapper") // @Autowired // private net.lunalabs.central.mapper.oracle.MeasureDataMapper oracleMeasureDataMapper; // // @Autowired // private net.lunalabs.central.mapper.oracle.PatientMapper oraclePatientMapper; // // @Autowired // private GlobalVar globalVar; 1. CentralStation에서 FTP Server를 만드는 데 있어서, 파일 경로를 설정해주는 데 이슈가 있었다. 테스트 배포서버의 OS는 우분투인데 현재 내 윈도우 OS 경로의 FILePath를 지정해주면 OS간 경로 특수문자가 달라서 파싱이 여의치 않았다.. 이 부분은 GW 쪽 FTP protocol를 사용해서 파일 전송하는 부분에서 코드를 수정해줘야 했다. ```java String pattern = Pattern.quote(System.getProperty("file.separator")); logger.debug("filePath: " + filePath); File fileTest = new File(filePath); String fileName = fileTest.getName(); public static String parsingFilepath(String string) { if (string == null || string.length() == 0) { return ""; } StringBuffer sb = new StringBuffer(); char c = 0; int i; int len = string.length(); for (i = 0; i < len; i += 1) { c = string.charAt(i); switch (c) { case '\\': log.debug("...." + c); sb.append("\\" +File.separatorChar); break; default: sb.append(c); } } return sb.toString(); } String parseFilepath = Common.parsingFilepath(strMessage); logger.debug("escapeFile: " + parseFilepath); obj = (JSONObject)parser.parse(parseFilepath); String filename = String.valueOf(obj.get("filename")); //여기서 문제. logger.debug("ftp uploder로 넘겨주는 filename: " + filename); 유독 특수문자 관련하여 파싱 부분에서 신경쓸 필요가 많았다.. 이런 로우레벨에 가까운 작업은 되도록 피하고 싶은 경험이었다.. socket 채널에서 읽는 메서드는 비동기 처리해야 여러개의 gw가 달라붙을 수 있었다. 이거는 항상 헷갈리는 점이다. SSE 프로토콜을 이 프로젝트에서 처음 써봤다. GW에서 던진 DATA를 C.S가 받아서 FrontServer로 계속 전달해서 그래프에 반영해줘야 되는데, 이 부분에서 처음 0.1초 단위로 프론트 쪽에서 ajax polling을 하는 식으로 구현할려고 하다가, 실시간 데이터 반영에 어울리지 않다고 판단, WebSocket을 쓸까 고민하다가, 측정데이터 같은 경우 굳이 양방향 통신이 필요없다고 판단. SSE를 쓰기로 결정했다. 이 프로토콜을 통해서 데이터를 받는 쪽은 프론트 개발자도 잘 모르는 영역이라 내가 대신 작업해줬다.. 처음 써보는 프로토콜이라 많이 헤멨다.. 프론트에서 전송받은 DATA를 그래프로 변환시키는데도 얘를 엄청 먹었다. 이게 realtime으로 계속 그려져야 되니, 처음에는 apex chart를 통해서 만들었는데, 배열에 x축과 y축을 집어넣는 방식이었다. 그런데 배열에 계속 값이 쌓이니, 어느순간 쌓이면 앞의 인덱스들을 지워주는 로직을 작성해야 한다. 이 부분도 프론트개발자가 잘 몰라서 내가 대신 만들어줬다..... 나머지 REST로 구현한 API 요청은 리덕스와 리덕스 사가를 통해서 구현하기로 했는데 이 부분도 프론트 개발자가 잘 몰라서 내가 대신 구현해주었다.. 이제 진짜 완성되었다고 쳤는데, 그래프가 버벅거리는 이슈가 발생했다. 알고보니 데이터를 업데이트 해주는 함수에서 useEffect를 custom한 hooks를 사용했는데, 같은 데이터가 들어올 때는 함수가 실행되지 않는 걸 확인할 수 있었다. 이 이슈도 내가 파악하고 해결해주었다.. 그렇게 함에도 그래프가 버벅거리는 이슈가 안 고쳐졌다. 혹시나 벡엔드 쪽 이슈 인가봐, nio socket에서 버퍼를 읽어오는 과정을 무한루프로 대체했다. 기존에는 버퍼를 쏘고 읽고 socket을 끊어버리고, 다시 연결하는 과정으로 만들었는데, 그런 부분들을 다 대체했다. 무한루프로 대체한 후 라즈베리파이에 심은 G.W가 C.S에서 쏘아준 Data를 버퍼로 다 못 읽어오는 현상이 발생했다. 희안하게도 pc에서 test 하면 잘 되는데, 라즈베리로 옮겨놓으면 버퍼 용량이 제한되는 현상이 발생했다. 에러 원인을 찾는데 꽤 긴 시간이 흐른 뒤, 버퍼 사이즈 자체를 줄이고, 무한루프로 읽어오는 것으로 대체함으로서 이슈를 해결했다. 이렇게까지 고쳐놓았음에도 불구하고 그래프가 버벅거리는 이슈가 해결되지 않았다. 테스트 결과, 프론트쪽에서는 데이터는 잘 받아옴을 확인했고 그래프를 그리는 쪽에서, 브라우저가 CPU 사양이 과도하게 잡아먹는 것을 확인할 수 있었다. 결국 graph 라이브러리를 교체하기로 결정했다. 위의 교체는 프론트개발자에게 맡겼는데 도통 진도가 나가지 않더라.. 결국 내가 canvas 태그를 활용한 chart.js와 chartjs-plugin-streaming 로 그래프를 교체햇다. 이 라이브러리는 x축에 realtime 옵션을 적용시켜서 지나간 데이터들은 자동으로 삭제되도록 만들어져있어 최적화에 용이하다고 판단했다. 이 과정에서 GPU를 활용한 WebGL 라이브러리로 교체를 시도해봤지만, 기존 chart 라이브러리와 달리 적용방식이 완전히 달라서 포기했다.. 교체 과정에서 Dom 에 직접 접근해야만 되는데 useRef 활용법을 몰라서 고생 좀 했다. 그래프를 교체하고 나니, 그래프 그려지는 것이 한 결 나아졌다. 하지만 스케줄러를 통해서, GW 메서드를 돌려서 실제 환경과 비슷하게 테스트를 해보니, Data 자체가 받아오는 쪽이 조금씩 delay 가 누적되는 현상을 발견했다.. 원래 delay는 발생하는 것이 당연한데, 누적이 된다는 것이 이상했다. SSE만 따로 단위테스트를 했을 시, 누적현상은 발생하지 않았다. 시간을 소모한 끝에 DB 쿼리의 문제라는 것을 확인했다. 문제는 관계형 DB 쿼리가 select를 제외한 경우 병렬처리가 안 된다는 이슈였다. 기존의 측정데이터 insert 로직은 대충 다음과 같다. QT에서 MS100 코드 전송시 => Gateway는 전송받은 JSON 데이타를 HL7 Protocol로 파싱 후 CentralStation에 전송 => CentralStation은 받은 측정데이터를 사내DB에 저장 => 병원DB에도 저장 => 측정데이터에서 보낸 pid를 기준으로 사내DB에서 환자정보를 가져옴 => 측정데이터와 환자정보를 JSON 문자열로 조합해 Front 서버에 SSE 프로토콜을 이용해(기기ID별로 발행주소) 전송 => 성공적으로 완료되면 HL7 Protocol data를 생성해 GateWay에 응답 => GateWay는 응답받았으면, 성공했다는 JSON 메시지를 QT에 응답 즉 0.1초마다 qt에서 측정데이터를 쏘면, 그때마다 c.s 가 그 측정데이터를 파싱해서, insert 7번*2 update, select를 하는데, 트랜잭션이 과도하게 발생한다는 점이다. 쿼리를 실행하는 메서드 자체는 비동기적으로 실행하여도 안에 IO 작업자체는 DB 내에 트랜잭션 단위로 lock이 걸려있어서 점점 더 느려지는 것이었다. 이 부분은 쿼리를 실행하는 메서드 자체를 비동기로 따로 빼놓은 후에, 전역 thread safe 한 hashmap을 하나 만들어서 측정데이터를 계속 add 해준다음 1분단위로 Bulk Insert 후 hashmap을 비워주는 형식으로 트랜잭션 수를 최소화했다.. 그리고 select 하는 부분의 쿼리메서드는 Caching을 활용해서 반복작업을 최소화하였다. 이제 드디어 그래프가 부드럽게 돌아가기 시작했다. 이 부분에서 또 한 가지 에로사항은 현재 개발환경에서 QT를 돌려볼 수 가 없어서, 내가 GW 측에서 가짜 데이터를 쏘아내는 스케줄러 함수들을 16개를 만들었는데, GW 하나가 16개를 돌렸을 시, GW의 threadpool이 감당하지 못하는 숫자의 thread가 쌓여서 예외가 터지는 상황이 발생했다. 이 이슈는 그쪽에서 QT 애뮬레이터를 exe 파일로 만들어줬고, i9 급 pc 2대에서 각각 8개, 6개, 나머지 pc에서2 대를 돌린다음 6개 돌린 pc 브라우저에서 그래프를 확인하니 부드럽게 돌아가는 것을 확인했다. 추가로 next.js frontServer를 배포할 때, 혹시 몰라 multicore 를 사용할 수 있도록, pm2 cluster mode로 배포하도록 설정했다. 이 부분도 내가 담당해서 진행했다. 추가적인 이슈는 그쪽 QT에서 보이는 그래프 파형과 프론트서버에서 브라우저로 그리는 그래프 파형이 일치하도록 내가 x축 값을 그쪽 QT에서 보내는 startTime과 endTime을 파싱해서 실기간으로 집어넣었는데.. 장비마다 시각 설정이 다르다고 한다더라.. 특정 기기에서는 인터넷 연결이 안 돼서 RTC가 안 된다고 하드라.. 그래서 x축을 프론트단에서 계산해서 집어넣었다. (현재시각 함수 사용- 밀리세컨드 반영) 이렇게 고생고생하며 완성했는데, 갑자기 배포한 180번 서버가 죽어버리는 현상이 발생했다. 알고나니 테스트 과정에서, 로그가 쌓이는데, 그게 파일에 계속 저장되어서 server가 뻑 가버리는 현상이었다. 이 부분은 배포과정에서.. log들을 null 로 처리해주도록 설정해서 이슈를 해결했다. QT를 만드는 쪽 서버에서 우리가 배포한 C.S 서버 도메인 주소로 소켓 연결을 못하는 이슈가 발생했다. 이 부분은 TCP 포워딩을 따로 작업을 해줘야 처리가 가능했다. 그리고, FTP protocol 접속 문제도 따로 포워딩 작업을 진행했어야 했다. 이 부분은 대표님이 담당하여 처리했다. 나는 네트워크 쪽 관련은 잘 모른다.. 소켓으로 원격서버 http 주소로 접속할려면 tcp 포워딩 해야됨.. 이거 말고도 자잘한 이슈들이 많았다. 자잘한 이슈들은 2차에서 해결하고, 이쯤에서 마무리짓도록 했다. DB 쪽은 그쪽에서 인터넷 접속이 안 되는 이슈가 발생해서, DB를 접근할 수 있는 서버에 TESTDB를 따로 설치하도록 DB 설치 가이드와 query문을 작성해서 보내달라고 했다. 나는 직접 설치하기보다 Docker가 깔려있다면 Docker로 띄워놓는 것을 추천해서 그 방식으로 가이드를 보내줬다.. 흠.. 또 다른 이슈가 분명 발생하겠지만.. 일단은 이쯤에서 마무리 되는 것 같다.

Spring @Async Annotation을 활용한 Thread 구현
spring에서 Async, 즉 비동기 기능을 사용하는 방법은 아주 간단하다. @EnableAsync로 비동기 기능을 활성화 비동기로 동작을 원하는 메소드(public 메소드)에 @Async 어노테이션을 붙여준다. 개요 Spring Async annotation를 이용하여 간단하게 비동기 처리가 가능하다.@async 를 선언한 메소드를 호출한 호출자는 즉시 리턴하고 실제 실행은 Spring TaskExecutor에 의해 실행.메서드는 Future 타입 값을 리턴하여 해당 Future에 get() 메서드를 이용하여 작업 수행을 할 수 있다. 스프링 TaskExecutor Executor는 스레드 풀의 개념으로 Java 5에서 도입된 개념. 구현체가 실제 Pool이라고 확신 할 수 없어 Executor(직역: 집행자)라 사용.TaskExecutor 인터페이스는 실행하는 Task를 받고 execute 메서드를 갖는다. TaskExecutor 종류 SimpleAsyncTaskExecutor이 구현에는 어떤 스레드도 재사용하지 않고 호출마다 새로운 스레드를 시작동시접속 제한(concurrency limit)을 지원 제한 수가 넘어서면 빈 공간이 생길 때까지 모든 요청을 block SyncTaskExecutor호출을 비동기적으로 수행하지 않는다. 대신, 각각의 호출은 호출 쓰레드로 대체된다. 간단한 테스트케이스와 같이 필요하지 않은 멀티쓰레드 상황에서 사용된다. ConcurrentTaskExecutorjava.util.concurrent.Executor Wrapper. SimpleThreadPoolTaskExecutorSpring의 생명주기 콜백을 듣는 Quartz의 SimpleThreadPool의 하위클래스.Quartz와 Quartz가 아닌 컴포넌트간에 공유될 필요가 있는 쓰레드 풀 ThreadPoolTaskExecutor자바 5에서 가장 일반적으로 사용.java.util.concurrent.ThreadPoolExecutor를 구성하는 bean 프로퍼티를 노출하고 이를 TaskExecutor로 감싼다. TimerTaskExecutor지원되는 구현물 중 하나의 TimerTask를 사용.쓰레드에서 동기적이더라도 메소드 호출이 개별 쓰레드에서 수행되어 SyncTaskExecutor와 다르다. WorkManagerTaskExecutor지원되는 구현물 중 하나로 CommonJ WorkManager을 사용Spring 컨텍스트에서 CommonJ WorkManager참조를 셋팅하기 위한 중심적이고 편리한 클래스SimpleThreadPoolTaskExecutor와 유사하게, 이 클래스는 WorkManager인터페이스를 구현하고 WorkManager만큼 직접 사용 @Async를 활용한 간단한 구현 1. AsyncConfigurer 구현 @Configuration @EnableAsync public class Config implements AsyncConfigurer { private static int TASK_CORE_POOL_SIZE = 2; private static int TASK_MAX_POOL_SIZE = 4; private static int TASK_QUEUE_CAPACITY = 10; private static String BEAN_NAME = "executorSample"; @Resource(name = "executorSample") private ThreadPoolTaskExecutor executorSample;} @Configuration 을 이용한 bean등록 EnableAsync를 이용하여 @async를 이용하겠다 알린다. AsyncConfigurer 구현 TASK_CORE_POOL_SIZE : 기본 Thread 수 TASK_MAX_POOL_SIZE : 최대 Thread 수 TASK_QUEUE_CAPACITY : queue 수 BEAN_NAME : bean 이름 실행할 테스크의 수는 QUEUE_SIZE+MAX_POOL_SIZE 보다 크면 안된다. POOL생성 과정1. 기본 thread(TASK_CORE_POOL_SIZE) 수까지 순차적으로 쌓인다2. 기본 thread(TASK_CORE_POOL_SIZE) 크기가 넘어 설 경우 queue에 쌓인다3. 큐에 최대치까지 쌓이면TASK_MAX_POOL_SIZE까지 순차적으로 한개씩 증가시킨다. 2. AsyncConfigurer 의 필수 Override 메서드 구현 @Bean(name = "executorTest") @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(TASK_CORE_POOL_SIZE); executor.setMaxPoolSize(TASK_MAX_POOL_SIZE); executor.setQueueCapacity(TASK_QUEUE_CAPACITY); executor.setBeanName(BEAN_NAME); executor.initialize(); return executor;}@Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new AsyncUncaughtExceptionHandler();} 위에서 설정한 상수들 할당하여 쓰레드, 큐, bean name을 설정한다. getAsyncUncaughtExceptionHandler 에 대해 설정 AsyncUncaughtExceptionHandler에 대해 구현해주어야 한다. 만약 멀티로 executor를 생성할 경우 @Override대신 @Qualifier를 선언해준다. 3. AsyncTask 생성 @Service("asyncTask") public class AsyncTask{ @Async("executorTest") public void executor(String str) { System.out.println("result:"+str); } } task 클래스 메서드 @Async 어노테이션에 Executor명을 적어준다. 4. 실행 public class AsyncController{ @Resource(name = "asyncTask") private AsyncTask asyncTask; @Resource(name = "Config") private Config config; @RequestMapping("/test.do") public ModelAndView doTask(HttpServletRequest request, HttpServletResponse response) throws Exception { asyncTask.executor("TEST"); }} 새로운 쓰레드가 생기며, test가 출력되는 것을 확인 할 수 있다. 스레드 결과 콜백 받기 public SocketChannel socketChannel2; //일단은 public으로 private boolean bLoop = true; @Async public CompletableFuture<SocketChannel> csSocketStart() throws IOException { socketChannel2 = null; // HL7 Test Panel에 보낼 프로토콜 socketChannel2 = SocketChannel.open(); logger.debug("central로 보내는 socket channel"); try { socketChannel2.connect(new InetSocketAddress("localhost", 5051)); logger.debug("socketChannel connected to port 5051"); socketChannel2.configureBlocking(true);// Non-Blocking I/O } catch (Exception e2) { logger.debug("connected refused!!!"); // e2.printStackTrace(); socketChannel2.close(); } return CompletableFuture.completedFuture(socketChannel2); } ---다른 클래스에서 호출 try { CompletableFuture<SocketChannel> completableFuture = csSocketService.csSocketStart(); SocketChannel channel = completableFuture.get(); //일단은 그냥 blocking 시켜서 보내자. 후에 thencombine으로 교체 System.out.println(channel); csSocketService.hl7ProtocolSendThread(sb.toString(), channel); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } [참고] ****자바와 스프링의 비동기 기술해당 포스팅은 토비님의 토비의 봄 TV 8회 스프링 리액티브 프로그래밍 (4) 자바와 스프링의 비동기 기술 라이브 코딩을 보며 따라했던 실습 내용을 바탕으로 정리한 글입니다. 실습 코드들은 IntelliJ를 이용해…** jongmin92.github.io ****[Spring 레퍼런스] 26장 태스크(Task) 실행과 스케줄링 :: Outsider's Dev Story이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다. 스프링 프레임워크는…** blog.outsider.ne.kr ****SPRING @Async를 활용한 multi thread 구현 - 2 - AsyncConfigurer 생성Spring 에서 비동기 처리를 하기 위해서 AsyncConfigurer@Asynk를 사용하려고 한다. 본 포스팅은 이번 시간에는 AsyncConfigurer 을 활용하여 Executor 를 생성하는 방법까지이다…** cofs.tistory.com https://pakss328.medium.com/spring-async-annotation을-활용한-thread-구현-f5b4766d49c5 https://jsonobject.tistory.com/233 주의 할 점은 private 메소드는 @Async 를 적용해도 비동기로 동작하지 않으며, 반드시 public 메소드에 @Async 를 적용해야 한다. self-invocation(자가 호출)해서는 안된다. -> 같은 클래스 내부의 메서드를 호출하는 것은 안된다. https://jeong-pro.tistory.com/187

connected-react-router
react-router-dom의 history.push와 connected-react-router의 push는 어떤 차이가 있나. 단방향의 흐롬(history -> store -> router -> components)을 통해 라우터 상태를 리덕스 스토어와 일치를 시켜줍니다. 단순히 history.push를 쓸 경우 마우스의 뒤로가기 버튼을 사용하거나 할때 혹은 비정상적으로 왔다갈다할때 가끔 오류가 발생하여(항상 발생하는 것은 아니고 정말 가끔), 안정화시켜주기 위해 connected-react-router를 사용합니다. https://it-eldorado.tistory.com/113

JPA 마이그레이션 Tip
회사에서 스프링 레거시 + myBatis로 이루어진 프로젝트를 부트 환경의 JPA로 마이그레이션 하면서, 내가 나름 고민한 일종의 Tip들을 적겠다. 엔티티 설계는 최대한 단순하게 단방향 연관관계로 베이스를 깔자 도메인 오브젝트는, 기존 테이블과 같이 만들었지만, 쓸데없이 의존관계를 갖고 있다면 과감하게 끊어서, 설계도를 최대한 단순하게 만들었다. 단방향 연관관계로만, 양방향 연관관계는 나중에 필요하다 싶을 시 고려를 하는 식으로 진행했다. 대부분의 경우에는 사실 양방향 연관관계까지 필요하다 싶은 경우는 없더라. 사실 아예 연관관계를 맺지 않아도, 세타 조인을 활용하거나, 정 안 되면 그냥 네이티브 쿼리를 사용할 수 도 있으니, 양방향 연관관계에 집착을 해서 엔티티 의존성을 복잡화 시킬 필요는 없어보인다. 마찬가지로 DB 제약조건들 (유니크 조건이나, 칼럼 길이 등.. )도 필수적인 걸 제외하면, 배제하고 작업을 했다. OneToOne 양방향 연관관계 & OneToMany 단방향 연관관계 주의 JPQL 로 쿼리를 짜다가, 직면한 문제인데, 분명 fetch join으로 최적화를 시켜줬것만. 쿼리를 실행할 때 자꾸 N+1 문제가 발생하는 게 아닌가. @OneToOne(fetch = FetchType.LAZY) //하나의 주문에 하나의 리뷰만 허용, 양방향, Review 엔티티 private Order order; ============================================ queryFactory .selectFrom(review) .join(review.order, order) .fetchJoin() .join(review.customer, customer) .fetchJoin() .where(review.order.restaurant.id.eq(restuarntId)) .limit(pageable.getPageSize()) .offset(pageable.getOffset()) .orderBy(review.id.desc()) .fetch(); 쿼리는 위와 같다, 자꾸 Delivery 쪽에서 N번만큼 쿼리가 나오길래, Order 엔티티를 살펴보니, ========================================= @OneToOne(fetch = FetchType.LAZY, mappedBy = "order") private Delivery delivery; delivery 엔티티와 양방향 연관관계를 맺고 있었다. 따라서 Lazy loading이 먹히지 않은 거.. Order 쪽 Delivery 를 주석처리해서 단방향으로 바꿔서 해결해줬다. https://dev-nomad.com/m/75 https://ocblog.tistory.com/70 // OneToMany 단방향의 단점 CASCADE 옵션은 정말 명확할때만 엔티티를 저장할 떄, 연관관계에 있는 엔티티가 영속 상태가 아니었을 시, 가장 편리한 수단 중 하나는 CASCADE 옵션을 사용하는 것이다. 하지만 이거는 많은 사람들이 경고하다시피, 하나의 엔티티가 다른 하나에 전적으로 종속적인 관계가 아닌 이상, 매우 주의해야 되는 옵션이다. 나는 그냥 배제하고 작업했다. orphanremoval 같은 것도 배제하고 진행. 상속관계 매핑은 자제 JPA를 공부하다보면, 누구나 상속을 활용한 다형성 쿼리를 짜고 싶은 욕심이 든다. 하지만 실제 사용하기는 까다로운데, 만약 여러테이블 전략을 사용한다면 다형성 쿼리는 사실상 상속구조에 속해있지만 필요없는 테이블들도 다 join 하게 될 것이고, 단일 테이블 전략을 사용했을 시, 칼럼구조가 지저분해지기 쉽상이다. 차라리 공통된 필드들은 @Embedded 같은 걸로 포함시키는 게 나을 듯 싶다. 엔티티 연관관계는 LAZY LOADING으로 고정 많은 JPA 책? 또는 블로그에서 ToOne 관계의 엔티티들을 조회할 떄는, 굳이 LAZY LOADING을 걸지 말고, 편하게 EAGER로 변경하라는 조언을 많이 한다. 하지만 그건 근본적인 해결책은 안 된다는 걸 알고, 언제든 서버에 불필요한 부하를 많이 줄 수 있다는 걸 알고 있다. 물론 지금 당장의 프로젝트에는 큰 차이가 없다는 걸 알고 있어도, 언제나 설계는 올바른 방향으로 진행하는 것이 나중의 스케일 확장에 도움이 된다는 걸 유념하며 LAZY LOADIN으로 고정했다. Open session in view 는 false로 고정 트랜잭션 커넥션을 너무 오랫동안 물고 있다는 김영한님의 조언 외에도(사실 지금 프로젝트는 Admin 사이트의 느낌이 강하다보니, 굳이 false로 줄 필요가 없음에도 불구하고) 코딩을 하는데에 있어서 일관성을 지키기 위해 False로 고정했다. 대부분의 필요한 오브젝트의 정보는 트랜잭션 내에서 구현할려고 노력했다. 이럴 경우 가장 골치아팠던 점이 어떤 하나의 엔티티를 조회해 그 오브젝트가 의존한 모든 엔티티 오브젝트에 대한 통계나 합을 구할려고 했을 때, toMany 관계의 의존 엔티티들은 fetch join으로 최적화가 안 된다는 점이었다. 이 부분은 toMany 관계의 연관관계 엔티티를 같이 조회할때는 lazy loading으로 구현하지만, 배치 사이즈 를 정해서, 최대한 n+1 문제를 없애도록 노력했다. jpa: hibernate: ddl-auto: update naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl open-in-view: false properties: hibernate.default_batch_fetch_size: 1000 hibernate: globally_quoted_identifiers: true #DB 예약어도 가능 Entity repository는 최소한만 스프링 데이터 JPA가 제공해주는 crud repository 인터페이스는 평소 사이드 프로젝트를 할 때는 유용하게 썼으나, 실제 프로젝트에서는 마이그레이션 하는 쪽 쿼리가 수백줄이 넘어가는 쿼리가 즐비하고 여러 테이블을 동시에 쓰는 경우가 많아서 한계가 있다고 판단했다. 네이밍 쿼리로는 그 모든 복잡한 쿼리를 표현하기는 힘들었고, 네이티브 쿼리를 쓰기에는 JPA의 DB 독립성 장점을 포기하는 셈이라 쓰기가 싫었다. 그래서 querydsl을 사용, 쿼리는 줄일 수 있으면 최대한 줄이도록 하고, 그럼에도 불구하고 네이티브 쿼리가 필요한 경우는, JDBCTemplate를 사용하는 쪽으로 가닥을 잡았다. 불필요한 인터페이스 사용 자제 인터페이스를 사용하면 여러가지 장점이 따른다. 객체지향의 SOLID한 원칙들도 인터페이스를 이용함으로써 많이 지킬 수 있게 된다. 하지만 늘 느껴왔던 거슬리는 점들은 직접적인 비즈니스 로직들이 추상화를 통해서 지나치게 숨겨지는 걸 목격하게 된다. 대부분의 레거시 코드에서 굳이 인터페이스를 만들 의미가 느껴지지 않음에도 관습적으로 인터페이스를 만들고 의존하는 걸 많이 보게 된다. 인터페이스를 구현한 빈들이 여러개가 있는 게 아님에도 불구하고 굳이 인터페이스를 만들어야 하나? 나는 확장성에 너무 신경을 써서 코드의 직관성이 더 떨어지는 것이 더 문제라고 생각한다. 인터페이스는 필요할 때 쉽게 생성할 수 있도록 유의를 줄 정도만 코드를 설계하면 될 터이다. 나의 개인적인 생각이다.. HashMap보다는 DTO 기존 레거시 코드는 컨트롤러간의 인자 전달을 모조리 HashMap으로 구현한 코드였다. 무슨 인자를 받는지 주석이야 적혀있긴 했지만, 모든 개발자가 공감하다시피, 주석은 믿을만한 놈이 못된다. 주석에 뒷통수 크게 데여본 경험이 한둘이어야 하지.. 따로 전달받은 인터페이스 명세서도 없는 셈이라. 결국 코드 한 줄 한 줄 뜯어보면서 예측을 해 볼 수 밖에 없었다. 이 밖에도 DTO를 만들지 않았을 때의 단점은 여러가지가 있다. 미리 Validation을 걸어볼 수 도 없고.. 그래서 request 객체 같은 경우는 모두 DTO로 변환을 하였다. 하지만 response 시 굳이 모든 응답 객체를 DTO로 정의하지는 않았다. 어차피 open session in view가 false라 view단에서 lazy loading 이슈도 안 생기고 해서, 가급적이면 DTO 로 반환 하지만, 엔티티 객체 그대로 반환하는 경우로도 많이 구현하였다. (사실 귀찮은 게 좀 컸다.) dto 변환 시 modelmapper를 썼는데, 성능상 이슈가 신경쓰이다면, mapstruct로 교체 고려 중.. 아직은 그렇게 신경 쓸 필요성을 못 느껴서 교체 안함. https://mangkyu.tistory.com/164 DB 쪽 연산 최소화 1 레거시 코드를 보면 항상 느끼는 생각이, 왜 DB에 이렇게나 많은 비즈니스 로직이 관여할까? 한국 웹 생태계는 데이터베이스 중심으로 잡혀있다. 하나의 페이지에 나타내야 할 복잡한 데이터들이 있을 경우, 어떻게든 한방 쿼리로 해결해서 뿌려주는 것이 지금까지 봐왔던 코드들의 공통된 특징이다. 조인에 조인, 서브쿼리에 서브쿼리.. 프로시저..어쩔 때는 SQL 쿼리 하나 이해하는 데 하루를 소모하기도 한다. 스프링 같은 미들웨어의 역할은 단순히 DB에 파라미터를 넘겨주고 응답값을 받아서 뷰단에 뿌려주는 역할로 그치는 경우로 제한된다. 내가 나름 생각한 이유는 초기 한국에는 어플리케이션 아키텍처에 대한 전문가 포지션 공급이 적었고, 대신 DB쪽 전문가는 많았다. 그래서 서버를 개발하는 측면에서 DB 쪽 중심으로 굴러갈 수 밖에 없었단 게 첫번째 이유. 두번째는 SI 업계 특성상, 갑이 요구하는 설계가 시도때도 없이 변경이 되는 경우가 잦다. 나 같은 경우도 소프트웨어 테스트 하루 전날, 요구사항이 바뀌는 것을 경험하기도 했다.. 그럴 경우 어플리케이션 자체가 비즈니스 로직에 밀접하게 연결이 되어있을 경우, 수정사항이 쉽게 반영이 되기도 힘들기도 하고, 대신 미들웨어가 단순히 통로 역할만 하고, 모든 연산을 DB쪽에 처리한다면, 변경사항이 있을 시, 쿼리 하나만 바꿔도 되니까 뭐 이런 식으로 관습이 고착화되지 않았나 생각한다.. DB 쪽 연산 최소화 2 그러면 이런 DB쪽 연산을 최소화해야 되는 이유는 뭘까? 일단 가독성이 떨어진다. 나는 자바 개발자이다. 물론 그 외에 이것저것 많이 하고, 데이터베이스에 대한 지식이 없는 것도 아니지만, 전문 DBA 수준에는 한참 못 미친다. 나에게 익숙한 환경이 내가 개발하기 편한 환경인 셈이다. 쿼리 하나에 1000줄이 넘어가는 경우, 보기만 해도 속이 울렁거리는 감각을 느낀다. 이어서 연결하여 유지보수성이 떨어진다. 어플리케이션 내에서 작성한 코드는 아무리 코드가 개판이어도, 브레이킹 포인트 잡고 디버깅 하면서 어느정도 감이라도 찾을 수 있다. 하지만 대부분의 쿼리는 디버깅하기도 용의하지 않고(운영계에서 잘못 건드렸다가 대형참사가 난다..), 주석도 거의 적혀있지 않거나 쓸모없는 내용만 적혀있다. 클라우드 환경에서도 불리하다. 아시다시피 DB에 대한 자원은 비싸다. 반면 WAS의 자원은 비교적 저렴하다. DB쪽에서 주요 비즈니스 로직이 몰려있는 환경은 나중에 스케일 아웃하기 불리하다. 뭐 이것은 나도 직접 경험한 건 아니고, 주위에서 이런 단점들이 있다쿠나 하고 들은 셈이지만.. 아무튼 DB에 로직이 몰린 것 자체가 나에겐 극혐이다 ㅠㅠ 한방쿼리 집착 ㄴㄴ 이거는 성능에 대한 잘못된 접근이라고 생각한다.. 성능을 무조건 I/O 횟수를 줄이는 수단으로, 한방쿼리로 해결하려는 잘못된 생각. 나도 솔직히 한방쿼리에 대한 집착이 있는 놈이라, 이 집착을 못 놓긴 하지만 그럴 때마다 김영한님의 안티 SQL? 그 얘기를 유념하고 접근할려고 노력했다. 쿼리 하나에 수백줄이 넘어가는 쿼리 같은 경우, 일단 생각해봐서, 줄일 수 있으면 줄이고, 그래도 안 돼면 쿼리를 나눠서, 어플리케이션 단에서 조립하는 쪽으로 방향성을 정함 https://scidb.tistory.com/entry/한방-Query를-사용하지-말아야-할-때 https://okky.kr/article/734539?note=2015422 정리 마이그레이션을 하면서, 아래 영상의 내용을 많이 곱씹었다. 꼭 필요한 의존관계가 아니면, 의존관계를 최대한 줄이도록 설계하라고 나름 노력했다.. 맞게 설계를 한 건지는 의문점이 남지만.. https://www.youtube.com/watch?v=dJ5C4qRqAgA&t=1174s&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9CTech https://www.youtube.com/watch?v=00qwDr_3MC4&t=7286s&ab_channel=TobyLee 지금까지 내가 나름 노력하면서 지키려고 생각했던 포인트들이었다. 이 노력이 헛짓거리가 안 되도록..

Thymeleaf 사용할 때, org.thymeleaf.exceptions.TemplateInputException: Error resolving template
이메일 템플릿으로 타임리프를 사용해 전송 중 Local에서는 정상적으로 날라가는데, 리눅스 환경에 배포 시 template 조각을 찾을 수 없다는 에러가 떴다... 경로상의 문제인 것 같아.. / 를 제거해봤다. 이제는 배포환경에서도 잘 돌아간다..

AntD Upload With Form / Multipart-form
import { Form, Input, Button, Radio, Select, Cascader, DatePicker, InputNumber, TreeSelect, Switch, Checkbox, Upload, UploadProps, } from 'antd'; const {TextArea} = Input; const onFinish = async (values: any) => { console.log('Success:', values); const form = new FormData() for (const [key, value] of Object.entries(values)) { if (value === undefined) { continue; } if (key === "songFile") { console.log("songFile", values[key]) form.append(key, values[key].fileList[0]?.originFileObj); continue; } console.log("why???", key) form.append(key, values[key]); } try { const promise = await postSongApi(form); console.log("promise", promise); navigate("/songs"); } catch (err) { alert("업로드에 실패하였습니다.") throw err; } }; const onFinishFailed = (errorInfo: any) => { console.log('Failed:', errorInfo); }; const uploadProps: UploadProps = { //maxCount: 1, //multiple: false, onRemove: file => { const index = fileList.indexOf(file); const newFileList = fileList.slice(); newFileList.splice(index, 1); setFileList(newFileList); }, beforeUpload: file => { setFileList([...fileList, file]); return false; }, fileList, }; <Form labelCol={{span: 4}} wrapperCol={{span: 14}} layout="horizontal" onValuesChange={onFormLayoutChange} onFinish={onFinish} onFinishFailed={onFinishFailed} disabled={componentDisabled} > <Form.Item label="Name" name="name" rules={[{required: true, message: 'Please input your song name!'}]}> <Input/> </Form.Item> <Form.Item label="audioType" name={"audioType"}> <Select> {(Object.keys(AudioType) as Array<keyof typeof AudioType>)?.map((audioType: any, index: any) => ( <Select.Option key={index} value={audioType}>{audioType}</Select.Option> ))} </Select> </Form.Item> <Form.Item label="copyRightHolder" name="copyRightHolder" > <Input/> </Form.Item> <Form.Item label="음원" name={"songFile"}> <Upload {...uploadProps}> <Button icon={<UploadOutlined/>}>Select File</Button> </Upload> </Form.Item> <Form.Item label="duration" name="duration"> <InputNumber/> </Form.Item> <Form.Item label="desc" name="desc"> <TextArea rows={4}/> </Form.Item> <Form.Item label="Button"> <Button htmlType="submit">등록</Button> </Form.Item> </Form>

logback-spring.xml 설정방법
여러가지 logging 라이브러리가 있는데 Spring Boot 2.0에서는 사진과 같이 Java Util Logging, Log4j 2, Logback 을 기본적으로 사용할 수 있다. 다른 로깅 라이브러리로 갈아타기 편하기 위해 Facade 패턴을 적용한 Slf4j를 사용하기 위해 lombok을 디펜던시에 추가해야한다. Lombok 이 제공하는 @Slf4j 어노테이션을 적용하면 log.debug() 만으로 쉽게 로그를 확인할 수 있다. Spring Boot 에서는 logback 이 기본 로깅 프레임워크로, 의존하고 있는 slf4j api 와 bridge 모듈을 함께 포함하고 있어 로그 처리 관련 모듈을 추가하지 않아도 된다. 기본 설정된 logbackPermalink Springboot 에는 기본적으로 logback 이 포함되어 있습니다. 따라서 Springboot 의 *Application.java 파일의 main()함수를 호출하여 실행할 떄, IDE의 console 창에 나타나는 내용이 그것입니다. https://linkeverything.github.io/springboot/spring-logging/ logback 관련된 설정을 찾다보면 logback.xml vs logback-spring.xml을 보게 되는데 Web Application이 시작되고 나서 classpath 내의 logback.xml을 뒤져서 환경 설정을 적용한다. 이 때는 Spring이 구동되기 이전이라 application.properties 내에 존재하는 값들을 불러올 수 없다. logback-spring.xml에서는 Spring이 구동된 후라 application.properties에 있는 값들을 불러올 수 있다. spring-boot-start-web 안에 spring-boot-starter-logging에 구현체가 있습니다. Logback을 이용하여 logging 하기 위해서 필요한 주요 설정 요소로는 Logger, Appender, Encoder의 3가지가 있습니다. Spring이나 일반 java 프로그램의 경우 logback.xml 파일을 resources 디렉터리에 만들어서 참조하지만 Spring Boot의 경우에는 아래 3가지 중 한 가지 방법을 선택합니다. application.properties에 설정 resources/logback-spring.xml에 설정 resources/logback.xml에 설정 Log level TRACE < DEBUG < INFO < WARN < ERROR https://yjh5369.tistory.com/entry/스프링부트-Spring-Boot-로그-설정-Logback https://perfectacle.github.io/2018/07/22/spring-boot-2-log/ https://goddaehee.tistory.com/206?category=367461 https://programmer93.tistory.com/46 [ 로깅 패턴 ] %-5level : 로그 레벨, -5는 출력의 고정폭 값(5글자) (INFO, ERROR, DEBUG , 기타 등등이다.) %d{날짜 형식 포맷} : 로그 기록시간 %d{yyyy-MM-dd HH:mm:ss} 을 사용하면 된다. %thread : 현재 Thread 명 %F : 로깅을 발생시킨 파일 명 %M : 로깅을 발생시킨 메소드 명 %logger{length} : Logger name을 축약할 수 있다. length는 최대 자릿수이다 (0 = 무제한) %line : 로깅이 발생된 라인 넘버%msg : - 로그 메시지 %n : 줄바꿈(new line) 이 외에도 여러 가지가 있다. ( http://logback.qos.ch/manual/layouts.html ) 콘솔 색깔 logback-spring.xml 색깔 적용 예시 <!-- 이 속성 안 쓸거임 --> // <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){green} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/> // <property name="CONSOLE_LOG_CHARSET" value="${CONSOLE_LOG_CHARSET:-default}"/> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <withJansi>true</withJansi> <encoder> <pattern>[%d{yyyy-MM-dd HH:mm:ss}:%-3relative][%thread] %green(%-5level) %logger{35} %cyan(%logger{15}) - %msg %n</pattern> </encoder> </appender> // %green(%-5level) %logger{35} %cyan(%logger{15}) 요런 식으로 색깔을 줄 수 있다. %highlight를 이용하여 로그 레벨에 따른 색을 줄수 있다. %black, %red, %green, %yellow, %blue, %magenta, %cyan, %white, %gray, %boldRed, %boldGreen, %boldYellow, %boldBlue, %boldMagenta, %boldCyan, %boldWhite를 이용 할 수도 있다. 적용 범위는 ()로 %highlight([%-5level]) 이와 같이 사용 가능하다. 9. JANSI on Windows While Unix-based operating systems such as Linux and Mac OS X support ANSI color codes by default, on a Windows console, everything will be sadly monochromatic. Windows can obtain ANSI colors through a library called JANSI. We should pay attention to the possible class loading drawbacks, though. We must import and explicitly activate it in the configuration as follows: Logback: <configuration debug="true"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <withJansi>true</withJansi> <encoder> <pattern>[%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n</pattern> </encoder> </appender> <!-- more stuff --> </configuration> https://www.baeldung.com/spring-boot-logging https://oingdaddy.tistory.com/257 https://hue9010.github.io/etc/logback-설정하기/ log4j2-spring.xml https://hermeslog.tistory.com/451 Log4JDBC https://congsong.tistory.com/23 spyLogDelegatorName does not allow to valid 뭐시기 나 같은 경우, log4jdbc.log4j2.properties 파일에 띄어쓰기가 적용되어서 에러가 터졌다. log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator https://hermeslog.tistory.com/454?category=302344 <?xml version="1.0" encoding="UTF-8"?> <!-- 60초마다 설정 파일의 변경을 확인 하여 변경시 갱신 --> <configuration scan="true" scanPeriod="60 seconds"> <!--로그 파일 저장 위치 --> <springProfile name="dev"> <property name="LOGS_PATH" value="./logs" /> </springProfile> <!-- 이 속성 안 쓸거임 --> <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){green} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" /> <property name="CONSOLE_LOG_CHARSET" value="${CONSOLE_LOG_CHARSET:-default}" /> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <withJansi>true</withJansi> <encoder> <pattern>[%d{yyyy-MM-dd HH:mm:ss}:%-3relative][%thread] %green(%-5level) %logger{35} %cyan(%logger{15}) - %msg %n</pattern> </encoder> </appender> <appender name="DAILY_ROLLING_FILE_APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOGS_PATH}/logback.log</file> <encoder> <pattern>[%d{yyyy-MM-dd HH:mm:ss}:%-3relative][%thread] %-5level%logger{35} - %msg%n</pattern> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOGS_PATH}/logback.%d{yyyy-MM-dd}.%i.log.gz </fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <!-- or whenever the file size reaches 100MB --> <maxFileSize>5MB</maxFileSize> <!-- kb, mb, gb --> </timeBasedFileNamingAndTriggeringPolicy> <maxHistory>30</maxHistory> </rollingPolicy> </appender> <logger name="net.lunalabs.central" level="INFO"> <appender-ref ref="DAILY_ROLLING_FILE_APPENDER" /> </logger> <!-- log4jdbc 옵션 설정 --> <logger name="jdbc" level="OFF" /> <!-- 커넥션 open close 이벤트를 로그로 남긴다. --> <logger name="jdbc.connection" level="OFF" /> <!-- SQL문만을 로그로 남기며, PreparedStatement일 경우 관련된 argument 값으로 대체된 SQL문이 보여진다. --> <logger name="jdbc.sqlonly" level="OFF" /> <!-- SQL문과 해당 SQL을 실행시키는데 수행된 시간 정보(milliseconds)를 포함한다. --> <logger name="jdbc.sqltiming" level="DEBUG" /> <!-- ResultSet을 제외한 모든 JDBC 호출 정보를 로그로 남긴다. 많은 양의 로그가 생성되므로 특별히 JDBC 문제를 추적해야 할 필요가 있는 경우를 제외하고는 사용을 권장하지 않는다. --> <logger name="jdbc.audit" level="OFF" /> <!-- ResultSet을 포함한 모든 JDBC 호출 정보를 로그로 남기므로 매우 방대한 양의 로그가 생성된다. --> <logger name="jdbc.resultset" level="OFF" /> <!-- SQL 결과 조회된 데이터의 table을 로그로 남긴다. --> <logger name="jdbc.resultsettable" level="OFF" /> <root level="INFO"> <appender-ref ref="STDOUT" /> </root> </configuration> https://exhibitlove.tistory.com/302 logback의 설정 항목 Level TRACE - DEBUG - INFO - WARN - ERROR 순으로 오른쪽으로 갈수록 높은레벨. 출력 레벨 이상의 로그만 출력한다. Appendar 이벤트마다 로그를 기록하는 기능을 처리하는 객체. 로그의 출력위치, 출력 형식등을 설정한다. logback-core모듈에는 3가지 기본 Appender이 있다. ConsoleAppender : 로그를 콘솔에 출력 FileAppender : 로그를 지정 파일에 기록 RollingFileAppender : FileAppender을 상속. 날짜와 용량등을 설정해서 패턴에 따라 로그가 각기 다른파일에 기록되게 할 수 있음.

병원관련 웹 기능 추가 및 수정
요구사항 개발완료된 병원 관련 웹 소프트웨어에서 기능 추가해달라는 요청사항이 들어왔다. 요청 사항은 크게 7가지였다. 기존의 블락킹이 걸리던 AI 분석요청을 비동기로 처리요구 비동기로 처리된 진행상황은 UI로 프로그레스바 표시 진행 상황은 클라이언트의 입장이 아니라 서버의 입장. 각각의 클라이언트가 브라우저로 5개의 요청을 보내면 서버 입장에서 표시되기 때문에 화면단에서 total 개수 10으로 표시 기존의 분석요청시 이미지 파일을 Base64 인코딩해서 쿼리스트링으로 보냈던 것을 multipart-Form 형식으로 바이너리 파일로 전송 Dicom에서 이미지 추출포맷을 jpg로 변경 selectBox 전체선택 기능 추가 서버 재부팅 시 NSSM 을 사용해 배치파일을 만들어 서비스 자동재시작 https://warpgate3.tistory.com/entry/java-jar-윈도우-서비스-등록하기 기존의 프로젝트는 springboot + jsp 형식으로 만들어졌다. 분석요청은 rest를 통해서 flask 서버와 통신해 그 결과를 불러와서 프로그레스바 진행상황을 업데이트하면 되었다. 간단한 기능추가 요구사항이었지만 개발환경이 갖추어지지 않아서 무척 고생을 했다. 환자 리스트를 불러와서, 분석요청을 해야되는데, 기존 연결된 병원 oracle DB는 현재상황에서 원격으로 접속할 수 없는 상황이었다. 가짜 테스트 DB를 만들어내서 병원 스키마 구조와 비슷하게 덤프를 떠서 작업 중, 계속 난항을 겪다가 결국에 운영계 DB와 직접 연결할 수 있는 환경의 서버에 원격접속하여 작업하는 것으로 변경됐다. (Chrome 원격데스트탑,mstsc ) 운영계 DB를 직접 건드린다는 게 부담스러웠고, 실제로 실수로 select를 제외한 쿼리문을 날려서 그쪽에 롤백을 문의하기도 했지만, 커다란 문제점이 없다고 답변하여 다행이었다. 개발과정 분석요청을 날리던 service layer의 method를 비동기 처리하였다. 프로그레스바 진행 상황을 표시하기 위해 전역변수로 Arraylist와 Interger를 만들었다. 비동기로 처리되기 때문에 threadSafe한 자료구조로 선언하였다. 원래는 진행상황을 표시할 DB Table을 하나 더 만들려고 했는데 그건 오바같아서 그냥 전역변수로 선언하였다. 뷰단에서 분석요청 버튼을 클릭하면 ajax로 컨트롤러를 때리고, 컨트롤러는 비동기 서비스 메서드를 호출하는 식인데, 컨트롤러를 때리면 바로 그 인자를 전역리스트에 넣고, 호출된 개수를 total로 숫자에 저장하였다. 그리고 바로 리턴 비동기 메서드 안에서는 rest 호출하고 그 결과값이 정상이면 추가된 전역리스트 값을 삭제, 아니라면 예외를 던짐 이렇게 변경되는 값들을 sse 프로토콜을 활용해 뷰단에 전달. 뷰단은 이벤트소스를 리스닝해 받은 값들을 가공해 UI 프로그레스바로 표시 헤맸던 사안 SseEmitter를 활용하여 서버에서 뷰로 단방향 통신을 하였는데, 전역 변수만 thread safe한 구조로 생각 중이다가 sseemitter 자체는 그런 구조로 설계를 못 하였다. 브라우저가 새로고침되거나, 새로 들어갈때마다 새로운 객체가 생성되는데 만료가 된 sseemitter 객체를 삭제할떄 exception이 터졌음. synchronized 메서드를 사용해 해결 파일을 multipart-from 형식으로 보낼때, postman으로 테스트하는 방법을 몰라서 고민, 진짜 별거 아니지만 생각보다 시간이 걸렸다.. 비동기 스레드로 분석요청을 하는 중 결과값으로 에러코드를 리턴받으면 예외상황이 터지도록 설계를 하였는데, @RestControllerAdvice 에서 예외를 낚아채지 못하는 상황. 조사해보니 비동기 예외처리는 AsyncUncaughtExceptionHandler 라는 인터페이스를 상속받은 객체를 등록해줘야지 낚아챌 수 있었다. 예외처리를 낚아채는 데는 성공했지만 이걸 다시 ServerSentEvent를 활용해서, 뷰단에 전달하는데는 실패. 시간을 꽤 많이 소모한 결과 AsyncUncaughtExceptionHandler 라는 인터페이스를 상속받은 객체를 @ApplicationScope 으로 등록해주어 해결하였다. bean의 scope과 관련된 문제로 보이는데 뭔가 감은 잡히는데 정확하게 이유는 파악하지 못했다. 기존 코드는 이미지를 생성할때, JPEGImageEncoder 를 사용하였는데, maven install 하여 war 파일로 압축하는 과정에서 문제가 생김. 표준 API가 아니라서 인식을 못하는 문제가 발생했다. ImageIo로 대체 jai_imageio-1_0_01-lib-windows-i586-jdk 를 사용하여 자바8 32 bit 에 등록하고 dicom 파일을 변환하는데, 이것도 buildpath에 등록하지 못해서 com.sun.image.codec.jpeg.JPEGImageEncoder를 찾지 못하였다. 배포시에 꼭 자바 풀경로 적어주고, buildpath도 명확하게 설정함으로써 해결하였다.

좋은 코드란? - 개인적인 생각.. 잡담
좋은 코드스타일, 좋은 아키텍처에 대해 얘기하는 수많은 글귀들이 있다. 대부분 다 동의하는 내용일 것이다. 지역범수의 범위를 최소화해라.. 함수의 의도를 명확히 해라... 테스트 케이스를 작성해라... 등등 일반적이고 원론적인 이야기들. 나는 자바 개발자라서 그런지 몰라도, 항상 직접적인 의존관계를 최소한으로 줄일려고 노력하는 편이다. 내가 봤던 대부분의 책은 객체지향을 기초로 써져있는 책들이다 보니, 추상화시키는 습관에 대해서 많이 강조를 하는 편이기 때문이다. 복잡하고 방대한 프로그래밍의 세상을 단순하고 직관적으로 바라보면서 의존관계가 변경이 되어도 클라이언트의 입장에서는 변동이 없도록, 물론 그렇다고 꼭 추상화가 좋은 것만은 아니라는 걸 알고있다. 지나친 추상화는 도리어 코드의 실제 의도를 저 밑바닥으로 감추고, 당췌 코드가 뭘 의도하는 건지 파악하기 힘들게 한다. 자바같은 OOP진영에서는 모든걸 추상화 하려고 시도한다. JPA 같은 경우도, 구체적인 쿼리를 코드상에서 드러나지 않게 밑바닥으로 감추지 않나. 이 부분을 싫어하는 개발자들도 분명 있을 것이다. 특정 기술이나 구현에 종속되는 걸 벗어나는 위주로 계속 발전을 하다보면, 결국 나중에는 개발을 하는 게 아니라, 그냥 툴 사용을 배우는 수준으로 변하지 않을까 걱정하는 글들도 봤다. 언제나 선이 중요한 셈이다. 내가 생각하는 적정선.. 별거 아닌 주니어 개발자지만, 나는 개인적으로 좋은 코드를 3가지 영역으로 구분한다. 가독성 생산성 효율성 아무리 개발실력이 뛰어나다고 하더라도, 누구나 직관적으로 알아보고 이해할 수 있는 코드를 작성하지 않으면, 그 개발자는 실력이 없다고 생각한다. 결국 개발은 혼자 하는 일이 아니므로, 다른 팀원이 유지보수할 수 있게끔 만들어줘야 할 필요가 있다. 코드가 좀 길어줘도 상관이 없다. 각 클래스, 함수마다 의도와 역할이 분명히 명시되고, 조립할 수 있게 모듈별로 쪼개도록 노력해야 한다. 본인이 아무리 실력에 자신이 있다고 하더라도, 이 부분을 신경 안 쓰면, 결국 시간이 지나고 다시 자기의 코드를 본다면 파악하기 힘들 것이다. 좋은 개발자는 네이밍컨벤션에서부터 그 흔적을 나타낸다. 가끔 네이밍컨벤션을 대수롭지 않게 생각하고, 자기 마음대로 짜진 코드를 보게 되는데, 볼때마다 참 곤혹스럽다. 네이밍컨벤션은 단순히 가독성만을 위한 게 아니다. 네이밍을 어떤 자기만의 규칙으로 일관적으로 짜게 되면, 이 공통된 특징을 이용해, 코드를 굉장히 유용하게 확장할 수 있다. 실제로 변수나 클래스에 대한 일정한 네이밍 규칙을 가지고, 기능을 구현하는 라이브러리, 프레임워크가 상당히 많다. 코드의 가독성이 확보된다면 주석 같은 경우도 불필요해진다. 이런 말하면 욕먹겠지만, 나는 주석을 굉장히 싫어한다. 주석보다는 테스트케이스를 훨씬 좋아한다. SI 개발자로 일하면서 주석에 뒷통수 데인적이 몇번 있다보니, (뭐 시나리오는 예상된다. 누군가 작업해놓은 코드를 후임자가 이어받았는데 수정해놓고 주석은 그대로 놨뒀겠지..) 주석을 신뢰하기보다, 무언가 독립적인 모듈로 짜져있어서 개별테스트하기 쉬운 코드조각을 훨씬 선호한다. 생산성이란, 객체지향적인 관점에서 코드를 유지보수하기 쉽고 확장가능하기 편하도록 만들어주는 걸 총칭하는 걸로 나는 여긴다. 이건 나만의 정의이다. 다른 사람들은 생각이 다르겠지.. 아무튼 보통 디자인 패턴에서 이런 경우를 찾아볼 수 있는 것 같은데, 아무래도 이거에 익숙하지 않으면 가독성이 떨어지는 부분도 분명 있단 말이지.. 효율성은 프로그램 입장에서의 효율성을 이야기한다. 보통 알고리즘에서 많이 찾아볼 수 있는 것 같은데, (물론 알고리즘을 더 넓은 추상적인 의미로 생각할 수 도 있고, 꼭 알고리즘이 효율성이라는 것도 아니지만..) 암튼 메모리 최적화, 시간복잡도 같은, 성능을 끌어올리는 관점이라고 나는 생각한다. 가독성이 좋다고 해서, 생산성과 효율성이 좋다고 보장할 수 없다. 그 반대도 마찬가지이다. 세마리 토끼를 다 잡는 게 제일 베스트이겠지만, 현실적으로 세 마리의 토끼를 다 잡는 것은 매우 힘들다. 이 세 마리 토끼는 서로 어떤 부분에서는 반비례 관계?를 가지게 되어서, 생산성을 너무 좋게 하려고 하다가 효율성을 포기할 수 밖에 없는 상황이 올 수도 있고..반대도 마찬가지.. 물론 이 세가지 다 안 좋은 코드도 존재하고, 이 세가지 항목을 모두 높은 수준으로 충족하는 코드도 있겠지만, 대부분은 그 사이 어디선가 타협점을 보게 된다. 나는 웹 같은 추상화된 프로그래밍 분야에서는 가독성이 그 무엇보다 우선시되어야 될 필요가 있다고 생각한다. 최적화는 그 다음의 일이다. 결국 돌고 돌아 선이 중요한 셈이다. 내가 생각하는 적정선.. 이 적정선은 결국 짬으로 해결을 할 수 밖에 없다고 생각한다. 언제나 상황은 케이스바이케이스니까, 어떤 전략을 선택할지는 경험밖에 답이 없는 것 같다.. 개인적인 똥글이었다..

WireShark
local 환경에서는 훅킹을 안 해서 감시를 못한 tcp - http - json https://www.wireshark.org/download.html

맥북 처음 써보는 맥찔이의 맥북에어 m1 개발환경 셋팅
https://unluckyjung.github.io/etc/2021/01/15/Mac-Init-Setting/ 위의 과정을 상당부분 참조했다. 맥북 애플계정 및 관리자 접근비번 맥북에어를 샀으면 애플계정을 만들어두자. 관리자 접근 비번은 터미널 root 계정의 비번이기도 하다. 만약 앱스토어에서, 정상적으로 애플계정을 입력했음에도 로그인되지 않고, 확인되지 않은 appleid입니다. 라는 메시지가 뜨면 아래와 같은 과정을 거쳐주자 Apple 메뉴() > 시스템 환경설정을 선택합니다. '로그인'을 클릭합니다. Apple ID와 암호를 입력합니다. 메시지가 나타나면 신뢰하는 기기 또는 전화번호로 전송된 6자리 확인 코드를 입력하고 로그인을 완료합니다. 기본 환경설정 셋팅 https://subicura.com/2017/11/22/mac-os-development-environment-setup.html 필수 프로그램 직전까지 따라해주면 된다. 추가적으로 나 같은 경우 단축키 설정에서 바탕화면 보기는 option + D, 미션컨트롤은 option+ A 자주 쓰는 응용프로그램 설치 postman 설치 lightshot 설치 설치했으면 꼭 보안 및 개인정보 보호 설정에서 접근 허용을 체크해주자. 추가적으로 카톡,노션, 화면분할을 위한 magnet(유료) 앱 등을 설치했다. 한글에서 백틱 나오게하기 https://ani2life.com/wp/?p=1753 맥에서는 new file이 없고 new folder만 우클릭으로 가능하다. 파일 만들려면 그 폴더 우클릭=>터미널 열기=> touch 명령어로 파일을 만들고 => vim 에디터로 파일 내용 작성 해야 한다. => OS 재시작이 필요 참고로 나 같은 경우는 단축키 설정에서 폴더에서 새로운 터미널 열기, comment + \ 로 등록해놈 설정 안 하고 한글에서도 백틱 쓰려면 option + ~ 해주면 된다. homebrew 설치 https://opentutorials.org/course/128/11129 homebrew에 대한 생활코딩영상 (터미널 아이콘 우클릭) -> (정보 보기) -> (Rosseta2로 실행) 체크 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" eval "$(/opt/homebrew/bin/brew shellenv)" //윈도우에서의 환경변수 설정과 비슷한건가? brew help brew install cask brew update m1 환경에서 brew 설치하기 https://designdepot.tistory.com/209 git 설치 brew install -s git git --version iterm2, oh my zsh 설치 https://pinkwink.kr/1354 iterm2 영상 설명링크 m1에는 기본적으로 zsh 환경으로 되어있다. brew install itrem2 iterm2도 로제타로 열기 체크하자 iterm 한글 깨짐 방지 profile > text > unicode > from을 NFC로 변경 iterm 꾸미기 https://jojoldu.tistory.com/428 iterm 접근 권한 주기 https://gitlab.com/gnachman/iterm2/-/wikis/fulldiskaccess 시스템 설정 > 보안 및 개인 정보 보호 > 개인 정보 보호 > 전체 디스크 접근권한 oh my zsh 설치, 터미널 꾸미기 sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" vi ~/.zshrc 상단에서 ZSH_THEME="robyrussell" 를 찾는다. ZSH_THEME="agnoster" 로 바꾸어준다. source ~/.zshrc D2Coding font 설정 OpenJDK 설치 https://memoming.com/16 인텔리제이 빌드가 너무 느려서 바꿔봤다. https://www.azul.com/downloads/?package=jdk#download-openjdk visual studio code 설치 역시 M1 용 설치 인텔리제이 설치 nvm, node.js 설치 yarn 설치 npm 명령어로 설치 mysql 설치 https://haddoddo.tistory.com/entry/MAC-MAC%EC%97%90%EC%84%9C-MySQLWorkbenchMySQL%EC%9B%8C%ED%81%AC%EB%B2%A4%EC%B9%98-%EC%84%A4%EC%B9%98-%EC%82%AC%EC%9A%A9%EB%B2%95 https://esjdev.tistory.com/10 command not found zsh: command not found: brew vi ~/.zshrc # export PATH=/opt/homebrew/bin:$PATH 추가 저장 터미널 다시 켜기 관련링크 https://www.dongyeon1201.kr/0f6baee0-0c28-477a-a31e-d1d8030741f2 Cannot install under Rosetta 2 in ARM default prefix 이슈가 생길 때

스프링 비동기 예외처리
https://stackoverflow.com/questions/51631641/cannot-autowire-service-into-implementation-of-asyncuncaughtexceptionhandler https://stackoverflow.com/questions/48006956/custom-asyncuncaughtexceptionhandler 비동기 코드는 별도의 스레드에서 실행됩니다(따라서 @ComponentScan새 스레드를 보고 초기화하기 위해). 기본적으로 모든 빈은 싱글톤입니다(자세한 내용은 봄의 빈 범위 참조). 새 스레드가 다른 컨테이너에서 실행되기 때문입니다. 서비스 빈에 대한 참조를 가져오지 않으므로 null이 됩니다. 왜 @ComponentScan처리 하지 않았습니까? 구성 요소 스캔은 대부분 패키지의 하위 집합을 스캔하는 것이며 서비스는 다른 패키지에 정의될 수 있습니다. 솔루션: @ApplicationScope어노테이션을 사용하여 서비스 Bean 애플리케이션 범위를 만드십시오 . @EnableAsync @EnableScheduling @Configuration public class AsyncConfig implements AsyncConfigurer { CustomAsyncExceptionHandler customAsyncExceptionHandler; @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setThreadNamePrefix("kang-"); executor.initialize(); return executor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { //return new CustomAsyncExceptionHandler(); return this.customAsyncExceptionHandler; } @Autowired public void setCustomAsyncExceptionHandler(CustomAsyncExceptionHandler customAsyncExceptionHandler) { this.customAsyncExceptionHandler = customAsyncExceptionHandler; } // // // @Override // public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { // return this.customAsyncExceptionHandler; // } @Bean public ThreadPoolTaskScheduler configureTasks() { ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); threadPoolTaskScheduler.setPoolSize(10); threadPoolTaskScheduler.setThreadNamePrefix("kang-scheduled-"); threadPoolTaskScheduler.initialize(); return threadPoolTaskScheduler; } } @Component @ApplicationScope public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler { @Autowired private Glovar glovar; @Autowired private CrescomAiService crescomAiService; @Override public void handleUncaughtException( Throwable throwable, Method method, Object... obj) { System.out.println("Exception message - " + throwable.getMessage()); System.out.println("Method name - " + method.getName()); for (Object param : obj) { System.out.println("Parameter value - " + param); } glovar.synchronizedList.remove(glovar.synchronizedList.size() - 1); glovar.errorCount.incrementAndGet(); try { //System.out.println("why?"); crescomAiService.serverSentProgress(); } catch (IOException e) { e.printStackTrace(); } } } https://www.baeldung.com/spring-bean-scopes @Singleton vs @ApplicationScope 4.3. 적용 범위 응용 프로그램의 범위는의 라이프 사이클에 대한 빈 인스턴스 생성 의 ServletContext를. 이것은 싱글톤 스코프 와 유사 하지만 빈의 스코프와 관련하여 매우 중요한 차이가 있습니다. 빈이 애플리케이션 범위일 때 빈 의 동일한 인스턴스는 동일한 ServletContext 에서 실행되는 여러 서블릿 기반 애플리케이션에서 공유되는 반면 싱글톤 범위의 빈은 단일 애플리케이션 컨텍스트로만 범위가 지정됩니다. 애플리케이션 범위로 빈을 생성해 보겠습니다 . https://stackoverflow.com/questions/44361580/is-there-application-scope-in-spring https://stackoverflow.com/questions/26832051/singleton-vs-applicationscope/27848417

리액트에서 특정 라우트에 들어가거나 나올 때 새로고침하는 방법
특정 페이지에서 나올 때 자동으로 새로고침을 해야 하는 경우가 생겼다. 일단 URL이 달라질 때마다 이벤트가 호출되는 방법을 찾았다. URL 값을 가져오기 위해 리액트 라우터의 useLocation이라는 훅을 사용할 수 있는데 BrowserRouter가 있는 컴포넌트에서는 같이 쓸 수 없었다. https://github.com/ReactTraining/react-router/issues/7015 그래서 BrowserRouter 컴포넌트의 자식 컴포넌트인 nav에 useLocation을 사용했다. import { useLocation } from 'react-router-dom'; nav 이하의 페이지가 URL에 따라 달라질 때마다 nav 컴포넌트에서 useLocation과 useEffect를 사용하여 이벤트가 발생하도록 했다. https://reacttraining.com/blog/react-router-v5-1/ let prePath = ''; // 컴포넌트 함수 외부에 위치 /* ... */ let location = useLocation(); useEffect(() => { if (prePath.indexOf('/player/') !== -1) { console.log('새로고침'); prePath = ''; window.location.reload(); // 새로고침 } prePath = location.pathname; // 지금의 주소를 저장한다. }, [location]); useLocation으로 받는 URL 곧 location이 달라질 때마다 useEffect가 호출되고 이전의 주소인 prePath에 /player/라는 문자가 들어갔을 경우에 새로고침 된다. https://thinkforthink.tistory.com/220?category=841733