💸딱 하루, 인프런 천원샵 오픈!

블로그

kailis

발자국 3주차: 테스트에서 가장 중요한 것

  테스트의 중요성? 사실 테스트가 [중요하다]는 것은 주입당한(?) 사상으로 어렴풋하게 알고 있었던 것 같다. 다들 중요하다고 하니까. 내가 보기에 그렇게 보이기도 하니까. 하지만 실무를 실제로 진행하면서 테스트를 작성할 수 있는 일 자체는 [일정]을 이유로 불가능했고,이는 테스트 코드 없이 실제 운영되는 코드를 작성하는 결과만을 낳았다. 돌이켜보면 이는 오히려 비효율적인 수동 반복 테스트를 낳기도 했다. 음, 그 시간에 차라리 테스트 코드를 짰다면... 나는 더 빠르게 테스트를 진행했을 수도 있다. 하지만 그동안은 테스트 코드가 [학습]의 영역이 아니어야 실무에 적용이 가능하다고 여겼다.(특히 일정이 이슈가 된다면 더더욱) 따라서 이번 기회에 [학습]의 꼭지점을 잡고, 프로젝트에 적용하는 것을 연습해 보기로 했다. 이번 프로그램을 신청한 목적은 [테스트] 였기 때문에 가장 초점을 두고 공부하고자 한 영역이기도 했다.  무엇을 테스트하는가? 우리는 프로그램을 만든다. 프로그램은 잘 돌아가야 한다. 잘 돌아간다는 것은 곧 정확하게 돌아가는 것과 같다 테스트는 프로그램이 [정확하게] 돌아가는 것을 보장하는 일이다.. 그렇다면 무엇을 기준으로 정확성을 판단할 수 있을까? 음... 내가 가장 어려워 하는 것은 이런 부분인 것 같았다. 테스트를 삼을 기준. 따라서 이번에는 어떤 것이 기준이어야 하는지 생각해 보았고, 그 결과는 다음 4가지로 귀결되었다. 기능적 요구사항 – 프로그램이 제공해야 하는 기능이 기대한 대로 동작하는가? 데이터 저장 후 올바르게 조회할 수 있는가? 성능적 요구사항 – 성능, 보안, 확장성과 같은 요소들이 기대치를 충족하는가? API 응답 속도가 1초 이내로 유지되는가? 동시 접속자가 많아도 서비스가 정상적으로 운영되는가? TPS는 원하는 대로 나오는가? 경계 조건 및 예외 처리 - 비정상적인 IO가 발생하는가? 입력 값이 비어 있거나, 허용 범위를 벗어난 경우에도 오류 없이 처리되는가?예외 시에 적절한 예외 반환을 보장하는가? 정확한 유효성 검사를 수행하는가? 데이터 무결성의 검증 - 트랜잭션은 작동했는가? 하나의 작업에 대해 의도한 모든 트랜잭션이 적절하게 적용되었는가? 실패한 프로세스는 없는가? 실패 시 Fallback이 정확하게 이뤄졌는가? 사실 무엇을 테스트할까 알아보면서 내리게 된 결론은.. 한 가지의 방향점을 가리키고 있었다. 지금 내가 세운 기준은 그동안 세워 왔던 단위 테스트 명세서에 들어 있던 것들이라는 걸. 왜 이것들을 진작 연결지어 보지 못했었는지.  테스트가 개발의 병목이 되지 않으려면 좋다. 이제 어떤 걸 테스트할지에 대해서는 명확해졌다. 강의에서는 [어떻게] 테스트를 하는지에 대한 방법론과 그걸 [할 때]의 효율성에 대해 다룬다. 강의를 보게 됨으로써 내 테스트는 한 층 더 정교해지고 빨라졌을 것이다. 그런데 나는 한 가지를 더 생각해 보고 싶었다. [무엇을 우선순위로 삼을 것인가.] 서두에도 언급한 만큼, 테스트 코드를 작성하는 것은 중요하지만, 현실적으로 일정이 촉박한 상황에서 테스트가 개발 속도를 저해하는 요소가 되어서는 안 된다. 따라서, 테스트를 작성하면서도 일정을 맞추는 방법을 고민해야 한다. 이럴 때 필요한 것이 우선순위가 아닐까 한다. 나는 이번에 다음과 같은 우선 순위를 결정해 보았다.  1. 핵심 로직만을 검증하는 Smoke Test(기본 동작 여부 확인)  모든 코드에 대해 100% 테스트 커버리지를 목표로 하면 개발은 지연될 수 있다. 따라서 일정이 촉박한 상황에서는 가장 중요한 핵심 로직과 장애 발생 가능성이 높은 부분만을 테스트 코드로 먼저 작성해야 한다는 생각이 들었다. 이를 비롯해 내가 처음에 이야기했던 [반복 수행될 수밖에 없는 케이스] 같은 곳. 어떠한 케이스 테스트를 코드를 수정할 때마다 돌려 봐야 한다면, 그 케이스들을 먼저 작성하고, 코드로 옮길 수 있는 부분들을 먼저 고려해 볼 것 같다.  2. 테스트 전략을 역할별로 나누어 적용하기 테스트를 작성할 때 모든 것을 하나의 방식으로 해결하려고 하면 시간이 오래 걸릴 수 있다. 이에 따라 역할별로 최소한의 전략을 선택하면 효율적인 테스트 작성이 가능할 것 같다. 단위 테스트(Unit Test)메서드 단위로 검증하여 빠르게 문제를 찾는다.검증이 잦게 필요한 곳이나 오류가 나기 쉬운 테스트에 우선 적용한다.통합 테스트(Integration Test)여러 모듈 간의 연동을 검증한다. 실행 속도가 느리므로 꼭 필요한 부분에만 적용한다.UI 테스트사용자 흐름을 검증하는 테스트로, 작성 및 유지보수에 시간이 많이 걸린다.핵심 기준을 명확하게 세우고, 비즈니스 로직과 분리해서 테스트한다. 즉, 단위 테스트를 우선 작성하여 빠르게 피드백을 받고, 일정에 여유가 있다면 통합 테스트를 추가하는 방식으로 접근하면 효율적일 것 같다. 또한 업무 단위로 테스트가 이뤄지는 곳은 UI 테스트가 통합 테스트로 여겨지기도 하기 때문에, 이러한 분리를 거친다면 검증에 대한 효율이 올라갈 듯하다.  테스트는 ##이다. 이번에 내리게 된 결론. 테스트는 결국 시간의 효율을 위한 것이다. 코드는 사람이 작성하는 것이며, 사람은 실수를 한다. 테스트는 이러한 실수를 빠르게 발견하고 수정할 수 있도록 도와준다. 좋은 테스트는 단순히 버그를 잡는 것이 아니라, 개발자와 사용자 모두에게 소프트웨어가 신뢰할 수 있는지에 대한 확신을 제공한다. 그 고민의 시간을 줄여 줄 것이다. 코드가 변경될 때마다 테스트를 통해 예상치 못한 문제가 발생하지 않는다는 것을 확인할 수 있으며, 이는 코드의 품질을 유지하는 가장 확실한 방법이다. 결국, 테스트는 단순한 검증 과정이 아니라, 코드의 신뢰성을 보장하는 핵심 요소이다. 이를 통해 개발자는 더 빠르고 안정적으로 코드를 수정할 수 있으며, 사용자 역시 신뢰할 수 있는 소프트웨어를 사용할 수 있다. 이 모든 것을 알면서도 주저할 수밖에 없는 것은 당장의 시간, 당장의 일정, 당장의 촉박함. 하지만 이번 회고를 토대로 나는 그러한 상황에서도 어떤 의사결정을 내릴지 결정할 수 있었다. 강사님은 말씀하셨다. 고용된 우리는 프로이며, 프로의 첫 의무는 일정 준수라고.  뼈에 오늘도 새겨 두면서 ..... 이제 앞으로 남은 강의와, 네 번째 발자국이 남아 있다. 3월의 마지막 주도 잘 갈무리해 보고자 한다.   

lkwo

워밍업 클럽 3기 BE 클린코드 3주차 발자국

3주차를 마무리하며...테스트 코드에 대해서 공부하는 시간을 가졌습니다.실무에서도 red green refactoring을 습관화해서 사용하면 심리적으로 안정감 있는 직장 생활이 될 것 같다는 생각이 들었습니다.강의 내용정리테스트 코드테스트 코드(자동화 테스트)가 필요한 이유수동으로 테스트를 진행하면 시간이 비효율적으로 사용됩니다.인수인계가 어렵습니다.수동으로 테스트하기에는 휴먼오류에 대한 리스크가 존재합니다.테스트를 통해 얻고자 하는 것빠른 피드백과 자동화 그리고 안정감을 얻을 수 있습니다.단위 테스트작은 코드 단위를 독립적으로 검증하는 테스트를 단위테스트라고 합니다.외부에 의존하지 않고 검증을 하도록 설계하여 속도가 빠르고 안정적입니다. 단위 테스트 작성할 때 주의점암묵적이거 아직 드러나지 않은 요구사항이 있는가? 에 대한 의문을 가지며 테스트 케이스를 세분화 하여야 합니다.해피케이스, 예외케이스에 대해서 고루 케이스를 세분화하여야 합니다.테스트하기 어려운 값 (외부 값, 시간)에 대해서는 분리하여 테스트에 용이한 구조로 만드는 것이 좋습니다. Layered Architecture와 테스트Persistence Layer Test쿼리가 의도대로 잘 작성이 되었는지 확인합니다.쿼리를 구현하는 기술이 바뀌어도 기능의 동작을 보장하도록 테스트를 작성합니다.@DataJpaTest를 사용해 테스트를 진행하는 경우에는 Persistence Layer에 필요한 빈들만 주입받으며, 자동으로 테스트 후에 roll back을 해줍니다.Business Layer TestPersistence Layer와 상호작용을 통해 비즈니스 로직을 전개하며, 이를 테스트 합니다.요청값부터 만들어서 비즈니스 로직을 테스트 합니다.트랜잭션을 보장하는지 확인합니다.Presentation Layer Test주요 로직은 business+persistence의 트랜잭션 경계내에서 끝나므로, presentation 에서는 외부에서 들어오는 값 검증을 주로 수행합니다.의존 관계를 가짜객체를 사용해서 환경을 재현합니다.@WebMvcTest, @MockBean, MockMvc 객체 등을 사용합니다.미션 Day 11저는 스터디 카페를 프로그램에 테스트 코드를 추가 하였습니다. [깃 허브](https://github.com/lkwoung88/readable-code/commit/7be5050a1c76e78809d85258aadbddfc3a430f3a) 계산, 판정 로직에 대해서 테스튼는 큰 고민없이 작성하였지만, 파일에서 읽어오는 부분은 어떻게 테스트하면 좋을지 고민을 많이 하다가 결국에는 작성하지 못했습니다.

웹 개발테스트코드

kailis

발자국 2주차: 읽기 좋은 코드 == 쓰기 좋은 코드

 읽기 좋은 코드, 쓰기 좋은 코드  이번 주의 발자국 회고. Readable 코드에 대한 이야기가 마무리되는 주간이다. 이번에는 강의 듣기와 함께 미션을 토대로, 내가 실제로 코드를 구현해 보는 시간을 가져갈 수 있었는데, 중간 점검 타임에서 과제에 대해서 조금 더 짚어주시면서 내 코드를 돌이켜보는 기회를 얻을 수 있었다. 사실 Spring과 JPA를 사용하는 평소 개발 방식은 많은 부분 "이미 모듈화된" 것들을 사용하는 경우가 많다. 특히 어노테이션을 쓴다든지 하는 케이스의 런타임 객체 관리 위임과 같은 상황에서 우리는 책임의 분리를 덜 고려하고도 편안한 개발을 할 수 있다. 단적인 예만 해도 Dispatcher Servelt은 우리의 xml 등록을 대리해 주고, 스프링은 Bean을 대신 관리해서 DL을 말려준다. (ㅋㅋ) 이런 것들이 감춰져 있기 때문에 우리는 서비스 로직을 조금 더 경량화할 수 있다. 그런데 기껏 스프링이 열심히 도와준 걸 망치면 안 되지 않을까? 필드를 얼마나 넣을 것인지, 이 객체의 책임 = 변경 가능 요소는 몇 개인지와 같은 것들을 잘 고려해 보면서 리팩토링 과제를 진행해 보았다. 그리고 이러한 미션을 점검하는 중간 회고. 중간 점검 회고 우선 중간 점검에서 했던 질문 시간이 꽤 재미있었던 기억이 난다. 멘토님의 커피 사랑 ㅋㅋㅋ 을 알 수 있는 좋은 기회이기도 했고... 나중에 게이샤 커피 꼭 먹어 보겠습니다. 공통 리뷰와 함께 개별 신청자에 대한 리뷰를 진행하는 2시간 가까이의 시간이었다. 목이 아프셨다고 했는데, 다음날 출근해서 말 한마디 못하시는 건 아니었는지 염려가 된다. 주요 내용은 장표를 공유해 주셨는데, 내가 이 내용 외에도 다른 분들 코드 피드백을 들으면서 인상깊었던 부분들을 정리해 보았다. 사물함 사용 가능 여부 등의 ENUM 처리: 추천컬렉션을 가공하는 로직이 생기면 일급 컬렉션을 고민해 볼 것Get / Set이 아닌 연상되는 단어 선택한 점이 좋음if-else보다 if-early return을 추천Mutable 컬렉션보다는 한번에 Immutable 컬렉션을 만들 것많은 클래스에서 사용한다 == 하나의 객체에 책임이 과도하게 몰려 있는 것은 아닌가? : 객체 분리의 신호탄IO 로직이 변경되어도 우리의 도메인 로직은 순수하게 보존되어야 함 이런 이야기가 있으면 나는 이중에서 나에게 도움이 가장 많이 된 것들을 꼽고는 하는데, 이번 글에서는 꼽을 수가 없다. 정말 모든 관점이 큰 도움이 되었던 것 같다.  강의 회고 하고 싶은 것 현재 진행 중인 실무 프로젝트에 배운 내용을 점진적으로 적용해보기"능동적 읽기" 방식으로 오픈소스 코드를 분석하며 좋은 패턴 학습하기 (TOBE - 진짜?) 이번 주간으로 <Readable Code: 읽기 좋은 코드를 작성하는 사고법>에 대한 강의가 끝이 났다. 사실 강의는... 진짜 솔직하게 말하면 아는 내용이 많다고 생각하면서 봤었는데. 실제로 코드를 리팩토링 하는 과정에서 그 생각이 제법 오만이라는 생각을 계속해서 하게 되었다. 역시 이론과 실제는 다르고, 이상과 활용은 천지차이다. 이번 강의와 미션을 통해 클린코드와 객체 지향 설계의 중요성을 실제로 체감할 수 있었다. 특히 회사에서 가장 의식적으로, 많이 노력하려고 했던 것. "코드는 작성하는 시간보다 읽는 시간이 훨씬 많다"는 강의 내용을 들었던 것을 염두에 두고 코드를 작성하고자 노력을 많이 했다. 사실 가장 어려운 부분은 적절한 추상화 레벨을 결정하는 것인 것 같다. 어떻게 인터페이스를 나누고 책임을 나눠야 하는지... 너무 세부적으로 메서드를 분리하면 오히려 코드 흐름이 파악하기 어려워질 것이고, 너무 크게 묶으면 단일 책임 원칙을 위반하게 될 것이다. 이러한 관점에서 하나 인상깊었던 것이 떠올랐는데, 멘토님이 예로 들어 주셨던 조건 분기문에 대한 코드 분리가 그것이다. 예를 들면, if(type.equals("blahblah")) 일 때 if(isEditable()) 으로 코드를 바꾸고, isEditable에 대한 함수를 하나 더 빼는 형식이다.  이 조건일 때 이런 행위를 한다는 분기를 하나의 함수로 표현하는 것.  사실 객체의 상태를 객체 안에서만 넣는 바람에 밖에서 나눠 볼 생각을 못했던 것 같은데.... 결국 코드는 1줄이나 2줄인 게 중요한 게 아니라, 들이는 공수를 대비해서, 로직을 망가뜨리지 않는 범위에서의 효율을 추구하는 일이라는 것. 이 균형을 맞추는 것이 클린코드의 핵심이라는 것을 조금 더 깨달았다.  읽기 좋은 코드 == (나중에) 쓰기 좋은 코드 따라서 이번에 정립하게 된 것. 읽기 좋은 코드는 동시에 쓰기 좋은 코드이다. 사실 읽기 좋은 코드를 짜다 보면 옆에서 "왜 굳이?" 라는 표현을 들을 수 있다는 생각이 든다. 그런데 나는 읽기 좋은 코드가 오히려 쓰기 좋은 코드라고 생각한다. 보다 정확하게 말하면, "내가 나중에 쓰기 좋은 코드"라고 생각한다. 단적인 예로 일급 컬렉션. 일급 컬렉션은 개발자의 책임을 줄여 로직이 망가질 확률을 최소화하는 일이다. 만약 특정 변수를 주입해서 판단하는 로직이 있다면, 그 로직은 변수에 종속적이며, 추후 개발자의 판단에 종속된다. 우리는 판단을 최소화함으로써 행동을 제약하고, 제약한 행동은 나중에 내가 쓰기 좋은 코드를 낳는다. 행복한 선순환이다. 하지만.  "오만"을 참는 법  처음부터 완벽한 코드는 오만이다 이번 중간점검 때 멘토님이 하신 말씀이 있다. 우리는 개발자고, 회사에 고용되어 일하는 것은 프로라는 뜻이다. 그리고 프로가 가장 중요하게 여겨야 할 것은 시간 관리다. 즉, 코드 퀄리티를 우선하다 주객이 전도되는 상황을 일으키는 것은 옳지 않다는 이야기다. 이러한 코드에 대해서는 추후 반영 시 가능하다면 리팩토링을 하는 식으로 점진적 개선을 할 수 있어야 한다. 이상적인 클린코드를 추구하다가 실용성을 간과한 경우를 자주 접하게 된다. 가령 진짜 if문 하나로 해결되는 문제를 객체로 분리했을 때 생기는 문제라든지.... 최신 트렌드의 개발에서 개념을 이해하는 것이 아니라 해당 개념을 적용하고만 싶어서 처리하는 케이스가 그런 상황을 발생시키는 것이 아닐까. 아주 자주 나오는 격언. "은탄환은 없다." 멘토님을 통해서 이번 기회에 또 한 번 상기할 수 있었다. 완벽한 동그라미의 바퀴를 만들고자 하지 말고, 굴러가게 만드는 것이 1번이다. 그 다음 세공하면 된다. 상황에 맞는 적절한 수준의 클린코드 적용을, 리팩토링을 위한 리팩토링을 계속해서 경계해야 한다고 또 한 번 다짐.  

팥우유

[인프런 워밍업 클럽 Full Stack 3기] 2주차

1. 학습 내용1.1. Supabase Storage파일과 이미지를 저장하고 관리하기 위한 서비스 (아마존 S3와 유사)1.1.1. 주요 개념버킷(Buckets): 파일을 논리적으로 구분하는 컨테이너객체(Objects): 저장된 개별 파일정책(Policies): 파일에 대한 접근 권한 규칙 2. 미션2.1. 미션 내용파일 목록에서 각 파일의 "마지막 수정 시간"을 표시2.2. 미션 진행- 리액트 쿼리로 요청, 응답 받은 이미지 데이터 객체 내 update_at 키-값을 이미지 컴포넌트에 표시함으로써 작업을 진행했습니다.3. 추가 학습 및 적용 기술3.1. pnpm 도입과 경험npm의 문제점(패키지 중복 설치, 디스크 낭비)에 불편함을 느껴 이번 기회에 pnpm을 도입했습니다. 설치 속도가 확연히 빨라진 것을 체감할 수 있었습니다.3.1.1. npm과 pnpm의 주요 차이점저장 구조와 디스크 사용량npm: 프로젝트마다 의존성 중복 저장으로 디스크 낭비pnpm: 전역 저장소에 패키지를 한 번만 저장하고 심링크로 연결하여 공간 절약성능npm: 중복 다운로드로 인한 네트워크 부하 및 시간 소요pnpm: 공유 저장소와 링크 기반 구조로 설치 속도 향상, 병렬 처리 최적화3.2. material-tailwind 경고 제거material-tailwind 컴포넌트 사용 시 발생하는 경고 메시지를 d.ts 파일을 통해 제거했습니다. 이 방법이 안전한지 고민했지만, 큰 문제가 없다고 판단하여 적용했습니다. 인프런 커뮤니티의 질문과 답변이 해결에 도움이 되었습니다.3.3. supabase storage type 적용db type과 달리 storage type은 supabase cli를 통해 자동 생성할 수 없다는 점을 알게 되었습니다. 대신 `@supabase/storage-js` 플러그인을 통해 필요한 type을 활용할 수 있었습니다.// UploadedImage Component import { FileObject } from '@supabase/storage-js'; // ...existing code... export default function UploadedImage({ file: { name, updated_at }, }: { file: FileObject; }) { return (// component) }3.4. prettier 설정GitHub Copilot의 도움을 받아 Next.js 프로젝트에 적합한 옵션으로 설정했으며, prettier-plugin-tailwindcss를 통해 Tailwind 클래스 자동 정렬 기능을 추가했습니다.// .prettierrc { "singleQuote": true, "semi": true, "useTabs": false, "tabWidth": 2, "trailingComma": "es5", "printWidth": 80, "arrowParens": "avoid", "jsxSingleQuote": false, "bracketSpacing": true, "bracketSameLine": false, "htmlWhitespaceSensitivity": "css", "requirePragma": false, "insertPragma": false, "proseWrap": "preserve", "endOfLine": "auto", "plugins": [ "prettier-plugin-tailwindcss" ] }3.5. eslint 설정@tanstack/eslint-plugin-query를 도입하여 React Query 사용 시 모범 사례를 따르도록 설정했습니다. 이를 통해 쿼리 키 검증, 의존성 확인 등의 이점을 얻을 수 있었습니다.// .eslint.json { "plugins": ["@tanstack/query"], "extends": ["next/core-web-vitals", "plugin:@tanstack/query/recommended"] } 4. 아쉽게 적용하지 못한 기술아래 3 가지 항목들은 모두 조사, 기능 개발 계획, 프로젝트에 일부 적용까지 하기도 했으나 시간이 부족해 결국 완성되지 못한 기능들입니다.4.1. 한글 파일명 업로드 문제 해결Supabase Storage에서 한글 이름의 파일을 업로드할 수 없는 문제에 직면했습니다. 원인은 파일명 인코딩 과정이 없어서 발생한 문제였습니다. 두 가지 해결책을 구상했지만 시간 부족으로 완성하지 못했습니다.파일정보 DB 테이블 접근법: 파일명과 UUID를 매핑하여 DB에 저장하고, 실제 스토리지엔 UUID로 업로드하는 방식customMetadata 활용: Supabase Storage의 메타데이터 기능을 활용하는 방식4.2. 직접 Tailwindcss 컴포넌트 구현 시도material-tailwind 대신 Tailwindcss만으로 모든 컴포넌트를 스타일링해보고 싶었으나, 시간 부족으로 실현하지 못했습니다.4.3. 컴포넌트 구조 설계의 고민개발자 관점에서 명확하고 구분하기 쉬운 파일 구조를 만들기 위해 다양한 React 컴포넌트 구조 패턴을 조사했습니다.각 패턴의 장단점을 분석하며 프로젝트에 가장 적합한 구조를 고민했지만, 시간 관계상 실제 적용은 제한적이었습니다.제가 찾아본 React 주요 패턴들은 다음과 같습니다. 이해를 돕기 위해 각 패턴마다 특징 및 예시 코드까지 준비하였으나 글이 너무 길어져 읽는데 어려움이 있을 것 같아 최종적으로 간단하게 한 줄로 요약했습니다.4.3.1. Presentational and Container Pattern로직과 UI를 분리하는 패턴으로, 재사용성과 테스트 용이성이 향상되지만 props drilling 문제가 발생할 수 있습니다.4.3.2. Compound Component Pattern복합적인 UI를 구성하는 관련 컴포넌트들을 그룹화하고 내부적으로 상태를 공유하는 패턴입니다. API 사용 경험은 향상되지만 TypeScript 타입 정의가 복잡해질 수 있습니다.4.3.3. Render Props Pattern컴포넌트의 렌더링 로직을 prop 함수로 전달하는 방식으로, 로직 재사용은 용이하지만 콜백 중첩으로 인한 디버깅 어려움이 있을 수 있습니다.4.3.4. Custom Hook Pattern로직을 훅으로 추출하여 여러 컴포넌트에서 재사용하는 패턴입니다. React의 핵심 패턴 중 하나로, UI와 로직의 분리가 명확합니다.4.3.5. Context API Pattern여러 컴포넌트에서 데이터를 공유하기 위한 패턴으로, props drilling을 방지할 수 있지만 불필요한 리렌더링이 발생할 수 있습니다.4.3.6. Atomic Design PatternUI 컴포넌트를 원자(Atoms), 분자(Molecules), 유기체(Organisms), 템플릿(Templates), 페이지(Pages)로 나누는 구조로, 체계적인 UI 구성이 가능하지만 초기 설계 시간이 많이 소요됩니다.4.3.7. Client/Server Component PatternNext.js 14 App Router의 핵심 패턴으로, 서버에서 데이터를 페칭하고 클라이언트에서 인터랙션을 처리하여 번들 크기를 최적화합니다.4.3.8. Server Components and Suspense Pattern데이터 로딩 상태를 선언적으로 처리하는 패턴으로, 점진적 UI 로딩을 지원하고 사용자 경험을 향상시킬 수 있습니다. 5.마무리이번 스터디를 통해 많은 것을 배우고 적용해보는 즐거움을 느꼈습니다. 계획했던 것보다는 적게 구현했지만, 새로운 기술들을 탐색하고 실험해본 경험은 매우 가치 있었습니다. 더 많은 공부 시간을 확보하니 다양한 시도를 해볼 수 있었지만, 동시에 욕심이 커져 모든 계획을 실현하지는 못했습니다. 3주 차에는 개인 약속으로 인해 학습 시간이 줄어들 것 같지만, 지금까지의 경험을 바탕으로 더 효율적으로 학습하고 구현해보겠습니다.무엇보다, 호기심을 가지고 새로운 기술을 탐색하고 적용해보는 과정 자체가 개발자로서 성장하는 중요한 발판이 된다는 것을 다시 한번 느낄 수 있었습니다. 

당황한 수달

[인프런 워밍업 클럽 3기] PM/PO 2주 차 발자국

PM/PO로 성장하기 위해, 고객과 데이터에 대한 전문성을 쌓는 것이 얼마나 중요한지 배운 한 주였습니다. 이번 주 강의(시작하는 PM/PO들에게 알려주고 싶은, 프로덕트의 모든 것)는 크게 두 가지 섹션(고객/데이터에 대한 전문성)으로 구성되었습니다.  2주 차 동안 무엇을 배웠는가?1. 고객 전문가가 되기 위한 접근법PM은 단순한 기획자가 아니라 고객의 문제를 해결하는 사람입니다.이를 위해 고객을 깊이 이해해야 하며, 그 방법 중 하나가 심층 인터뷰와 사용성 인터뷰입니다.강의에서는 단순히 "고객을 만나면 좋다"가 아니라,리서치를 왜 하는지어떤 목적을 가지고 진행해야 하는지각 인터뷰 방법을 어떻게 실행하는지그리고 구체적인 사례를 바탕으로 다뤄졌습니다.특히 튜터님이 직접 진행하셨던 실제 리서치 사례(모집 방법, 사전 질문을 통한 필터링 기법 등)를 공유해 주셔서, 단순한 이론이 아니라 실무에서 어떻게 활용할 수 있을지 감을 잡을 수 있던 소중한 시간이었습니다. 2. 목적이 있어야 의미가 있는 데이터PM이 데이터를 활용할 때도 "어떤 데이터를 모을 것인가"가 아니라 "이 데이터를 통해 무엇을 검증할 것인가"가 중요하다는 것을 다시한번 느꼈습니다. 단순히 데이터를 축적하는 것이 아니라, 이를 통해 의사결정을 내릴 수 있어야 한다는 점을 강조하셨습니다.2주 차 회고이번 주 강의를 통해 가장 크게 배운 것은 "무엇을 하든 목적을 명확히 해야 한다"는 점입니다.특히 저는 과거에 사용성 인터뷰와 심층 인터뷰를 진행한 경험이 있지만, 지금 돌아보면 아쉬운 점이 많았습니다. 당시에는 일단 하면 답이 나올 것이라는 막연한 기대만 있었고, 명확한 검증 목표 없이 진행했기 때문입니다.이번 강의를 통해 내가 했던 실수를 인지하고, 어떻게 개선할 수 있을지 고민할 기회가 되었습니다. 또한, 실시간 온라인 라이브에서 튜터님께 직접 질문하며 부족했던 부분을 보완할 수 있었던 것이 특히 좋았습니다. 다음 주 학습 계획은?다음 주는 지표를 깊이 탐구하는 강의로 구성되어 있습니다. 일정이 다소 빠듯하긴 하지만, 강의 목차를 보니 익숙한 개념들이 포함되어 있어 복습하는 마인드로 접근하려 합니다. 다만, 단순한 복습이 아니라 내가 미처 인지하지 못했던 핵심 포인트를 짚어내는 것에 집중하려고 합니다. 기존에 알고 있던 개념이라도 더 깊이 이해하고 실무에서 활용할 수 있도록, 중요한 부분을 꼼꼼히 파악하며 학습할 계획입니다.

기획 · PM· PO김민우튜터인프런워밍업클럽스터디PMPO

강동훈

[인프런 워밍업 클럽 3기 - CS] - 2주차 미션 (자료구조와 알고리즘)

재귀함수에서 기저조건을 만들지 않거나 잘못 설정했을 때 어떤 문제가 발생할 수 있나요?재귀함수는 함수 내부에서 자기 자신을 다시 호출하여 작업을 수행하는 함수를 의미한다. 즉, 자기 자신을 무한대로 호출하여 작업하기 때문에 함수 종료 조건인 기저조건을 설정하지 않는다면, 해당 함수가 실행됨에 따라 무한대로 콜스택에 메모리가 얹히게 되고 스택 오버플로우가 발생하여 프로그램이 강제 종료된다.// 기저 조건 없는 경우 function factorial(n){ return n * factorial(n - 1) } // RangeError : Maxmum call stack size exceeded // 기저 조건 설정 function factorial(n) { if (n == 0) return 1; return n * factorial(n - 1); }0부터 입력 n까지 홀수의 합을 더하는 재귀 함수를 만들어보세요.하위조건 : n - 1이 홀수인지 확인하고 홀수일 경우 n을 더하고 짝수일 경우 0을 더함기저조건: n이 0 이하일 경우 0을 반환하고 함수 종료function sumOdd(n){ // 재귀 로직 if (n <= 0) return 0; let oddNum = n % 2 === 0 ? 0 : n; return oddNum + sumOdd(n - 1); } console.log(sumOdd(10)) // 25다음 코드는 매개변수로 주어진 파일 경로(.는 현재 디렉토리)에 있는 하위 모든 파일과 디렉토리를 출력하는 코드입니다. 다음 코드를 재귀 함수를 이용하는 코드로 변경해보세요.const fs = require('fs'); // 파일을 이용하는 모듈 const path = require('path'); // 폴더와 파일의 경로를 지정해주는 모듈 function traverseDirectoryRecursive(directory) { const files = fs.readdirSync(directory); // 1. 인자로 받은 폴더 내부 파일들 추출 for (const file of files) { const filePath = path.join(directory, file); // 2. 파일 경로 합치기 const fileStatus = fs.statSync(filePath); // 2. 파일 정보 얻기 if (fileStatus.isDirectory()) { // 3-1. 폴더일 경우 재귀 console.log('디렉토리:', filePath); traverseDirectoryRecursive(filePath); } else { // 3-2. 파일일 경우 출력 console.log('파일:', filePath); } } } traverseDirectoryRecursive('.'); // 현재 경로의 모든 하위 경로의 파일, 디렉토리 출력하위 조건:인자로 받은 Directory의 파일과 폴더를 읽어온다파일 경로를 합치고 파일 정보를 얻어온다폴더일 경우, 재귀함수를 통해 내부 폴더의 파일과 폴더를 읽는다파일일 경우, 파일을 출력한다.기저조건:현재 폴더 내부 모든 파일 수만큼 반복📔 회고알고리즘 문제가 아닌 실전에서 사용할 수 있는 재귀 함수로 응용을 해보니 생각보다 하위조건을 파악하고 기저조건을 설정하는 것이 쉽지 않다는 것을 깨달았다. 처음에는 계속해서 코드를 읽어보면서 익숙하지 않은 fs모듈에 대해서 먼저 파악해보고, 제공되는 메서드들을 익혀보았다. 그렇게 코드의 흐름을 익혀가면서 반복되는 부분을 구분하였고, 재귀적으로 해결할 수 있는 부분은 while 문이라는 것을 파악했다. 기존에 스택을 통해서 파일들을 가져오고 데이터를 쌓아오면서 while 문을 통해 스택에 있는 데이터를 다시 출력하는 코드였다는 것을 파악하였고, 이를 재귀적으로 변경하기 위해서는 스택 자료구조를 사용하지 않고 하나의 함수에 하나의 폴더를 읽어오고 재귀적으로 함수를 다시 호출하면서 폴더 내부의 파일을 찾아가는 형식으로 수정할 수 있다는 것을 파악했다. 그렇게 하위조건을 설정하였고 기저조건을 만들어서 성공적으로 재귀함수로 코드를 수정할 수 있었다.이렇게 알고리즘을 응용하여 실전에서 사용할 수 있다는 것을 크게 깨달았고, 앞으로 알고리즘을 배울 때도 실전에서도 사용될 수 있는 다양한 사례를 함께 찾아보면서 공부하면 더 알고리즘 개념을 탄탄히 가져갈 수 있을 것 같다.

알고리즘 · 자료구조자료구조인프런워밍업

lkwo

워밍업 클럽 3기 BE 클린코드 2주차 발자국

2주차를 마무리하며...2주 차에는 예제 코드의 리팩토링을 직접 해보는 과제와 중간 점검이 있었습니다.강의를 보며 따라 할 때는 괜찮았지만, 막상 코드에서 리팩토링을 하려니 무엇부터 시작해야 할지 막막하더군요.리팩토링은 강의만 본다고 해결되는 영역이 아니라는 것을 느꼈고, 체화되기까지 시간이 필요하겠다고 생각한 한 주였습니다.강의내용 정리좋은 주석이란?주석이 많다는 것은 코드에 적절한 추상화를 적용하지 못해, 주석으로 코드를 설명하려는 것은 아닌지 의심해봐야 합니다.좋은 주석이란 코드로 전달할 수 없는 정보를 담는 것입니다.예를 들어, 의사 결정의 히스토리 같은 내용을 기록하는 것이 좋은 주석입니다.또한, 주석도 코드와 마찬가지로 버전 관리가 필요합니다. 관련된 의사 결정이 변경되었다면, 주석도 잊지 말고 함께 수정해야 합니다. 변수와 메서드 나열변수는 사용하는 위치와 최대한 가까이 두어 뇌 메모리 부담을 줄이는 것이 좋습니다.메서드와 변수의 접근자 종류에 따라 위치를 정리할 수 있습니다.메서드는 접근자 종류뿐만 아니라, 중요도와 로직의 종류를 기준으로 배치하면 더 정리된 코드를 만들 수 있습니다. 패키지 나누기패키지는 단순한 디렉터리가 아니라, 문맥적인 정보를 제공하는 역할을 합니다.적절하게 패키지를 나누면 코드의 가독성과 유지보수성이 향상됩니다.  IDE 활용하기정렬 단축키를 활용하여 코드 스타일을 통일합니다.Linting & 스타일 도구를 사용하여 코드 품질을 높일 수 있습니다.SonarLintEditorConfig리팩토링 직접 해보기리팩토링리팩토링에는 합리적인 이유가 필요하다리팩토링을 할 때는 단순히 코드 줄 수를 줄이는 것이 목표가 아니라, 객체의 역할과 책임을 올바르게 분배하고, 적절한 추상화를 적용하는지가 중요합니다. 이를 위해 리팩토링 과정에서 내가 합리적으로 수정하고 있는지 계속 의심하며 진행했습니다. (제가 정말 합리적으로 했는지는 모르겠습니다만...) 리팩토링 순서 정하기처음 코드를 봤을 때, 어디서부터 손을 대야 할지 막막함을 느꼈습니다. 그래서 나름의 리팩토링 순서를 정하고 진행해보았습니다.내가 읽기 좋은 코드로 변경하기공백 추가하기 (가독성 개선)함수 분리하여 추상화하기테스트하기 쉬운 구조로 변경하기역할과 책임을 고려하여 인터페이스와 객체로 분리하기 가독성 개선하기패키지 분리하기   리팩토링에는 정답이 없다리팩토링은 정해진 답이 없다는 점이 가장 어렵게 느껴지는 부분인 것 같습니다.  중간점검Day 4 미션 피드백아래는 제가 미션으로 제출한 코드입니다.이번 점검에 피드백으로 얻은 새로운 인사이트를 나열해봅니다.반환 타입이 boolean인 경우, 예외를 발생시키기 전에 해당 메서드의 사용 현황을 먼저 파악한 후, 상황에 맞게 리팩토링해야 한다.예외를 던지는 것은 비용이 많이 들 수 있으므로, 필요할 때만 신중하게 사용해야 한다.  private static boolean VALID = true; public boolean validateOrder(Order order) { if (order.isItemEmpty()) { throw new AppException("주문 항목이 없습니다."); } if (order.isTotalPriceLessThenZero()){ throw new AppException("올바르지 않은 총 가격입니다."); } if (order.hasNotCustomerInfo()) { throw new AppException("사용자 정보가 없습니다."); } return VALID; }Day 7 미션 피드백물론 리뷰를 할 때, 말을 조심스럽게 해야할 테지만, 코드 리뷰의 목적은 다 같이 좋은 코드를 만들어 보자는 좋은 의미입니다.코드 리뷰를 할 때는 인격적인 모독을 하지 않는다.코드 리뷰를 받을 때는 코드와 나를 동일시 하지 않는다.배운 점 정리 정적 메서드 팩토리를 만들 때는 생성자를 private으로 감춘다.null 대신 Empty 객체를 만들어서 처리한다.단수/복수를 신경 써서 변수명과 메서드명을 짓는다.리팩토링에는 정답이 없다. 하지만, 효율적인 구조와 다양한 의견이 있다. 

웹 개발클린코드

Yang HyeonBin

[인프런 워밍업 클럽 3기] 풀스택 과정 2주차 발자국 👣

2주차에 배운 내용을 정리해본다.깃허브 링크 1. 배운 내용1. Next.js에서 메타데이터 정의하기<meta> 태그를 이용해 사이트 정보를 정의하려면,서버 컴포넌트 파일에서Metadata를 정의해줘야 함// page.tsx import { Metadata } from "next"; import Ui from "./Ui"; // 페이지의 메타데이터를 정의 // use client에서는 사용 불가 - 클라이언트 코드는 Ui.tsx에서 정의하는 이유 export const metadata: Metadata = { title: "Dropbox Clone", description: "A minimalist Dropbox Clone", }; export default function Home() { return <Ui />; } 2. 파일 드랍 존 만들기<input /> 태그를 이용, type="file"3. supabase1. storage bucket 만들기업로드 가능한 파일 종류 설정 가능만들 때 Allowed MIME types 옵션에서 image/* 등의 조건을 추가하면 됨2. policy 생성사이드바 policy 메뉴에서 생성 가능이름, 가능한 액션 종류 선택, 누구에게 가능하게 할지 선택 가능4. 파일 드래그앤드롭 - react-dropzone 라이브러리 사용사용법은 npm 공식 문서의 코드 조각을 확인https://www.npmjs.com/package/react-dropzone  2. 이슈 사항1. storage의 get url 형태 변경: getImageUrl 함수 커스텀 어려움이미지의 만료일을 지정할 수 있게 변경됨그러면서 token이라는 서치 파라미터가 필수값으로 추가된 듯토큰을 누락한 형태로 확인 시 에러가 발생하며 이미지 로드 실패{"statusCode":"400","error":"Error","message":"querystring must have required property 'token'"} bucket을 public으로 전환하고, supabase에서 제공하는 getPublicUrl 메서드를 사용storage에서 bucket 이름 옆 드롭다운 메뉴 → edit bucket → public으로 설정 getImageUrl 함수 내부를 아래와 같이 수정const { data } = supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET!) .getPublicUrl(path); /** * A simple convenience function to get the URL for an asset in a public bucket. If you do not want to use this function, you can construct the public URL by concatenating the bucket URL with the path to the asset. * This function does not verify if the bucket is public. If a public URL is created for a bucket which is not public, you will not be able to download the asset. * * @param path The path and name of the file to generate the public URL for. For example `folder/image.png`. * @param options.download Triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename. * @param options.transform Transform the asset before serving it to the client. */ getPublicUrl( path: string, options?: { download?: string | boolean; transform?: TransformOptions } ): { data: { publicUrl: string } } { const _path = this._getFinalPath(path) const _queryString = [] const downloadQueryParam = options?.download ? `download=${options.download === true ? '' : options.download}` : '' if (downloadQueryParam !== '') { _queryString.push(downloadQueryParam) } const wantsTransformation = typeof options?.transform !== 'undefined' const renderPath = wantsTransformation ? 'render/image' : 'object' const transformationQuery = this.transformOptsToQueryString(options?.transform || {}) if (transformationQuery !== '') { _queryString.push(transformationQuery) } let queryString = _queryString.join('&') if (queryString !== '') { queryString = `?${queryString}` } return { data: { publicUrl: encodeURI(`${this.url}/${renderPath}/public/${_path}${queryString}`) }, } } StorageFileApi.ts를 참고하면 다양한 메서드가 있음 - createSingedUrl를 이용하면 expiresIn을 직접 지정 가능. 이걸 이용하면 bucket이 public이 아니어도 가능할듯 /** * Creates a signed URL. Use a signed URL to share a file for a fixed amount of time. * * @param path The file path, including the current file name. For example `folder/image.png`. * @param expiresIn The number of seconds until the signed URL expires. For example, `60` for a URL which is valid for one minute. * @param options.download triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename. * @param options.transform Transform the asset before serving it to the client. */ async createSignedUrl( path: string, expiresIn: number, options?: { download?: string | boolean; transform?: TransformOptions } ): Promise< | { data: { signedUrl: string } error: null } | { data: null error: StorageError } > { try { let _path = this._getFinalPath(path) let data = await post( this.fetch, `${this.url}/object/sign/${_path}`, { expiresIn, ...(options?.transform ? { transform: options.transform } : {}) }, { headers: this.headers } ) const downloadQueryParam = options?.download ? `&download=${options.download === true ? '' : options.download}` : '' const signedUrl = encodeURI(`${this.url}${data.signedURL}${downloadQueryParam}`) data = { signedUrl } return { data, error: null } } catch (error) { if (isStorageError(error)) { return { data: null, error } } throw error } } /** * Creates multiple signed URLs. Use a signed URL to share a file for a fixed amount of time. * * @param paths The file paths to be downloaded, including the current file names. For example `['folder/image.png', 'folder2/image2.png']`. * @param expiresIn The number of seconds until the signed URLs expire. For example, `60` for URLs which are valid for one minute. * @param options.download triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename. */ async createSignedUrls( paths: string[], expiresIn: number, options?: { download: string | boolean } ): Promise< | { data: { error: string | null; path: string | null; signedUrl: string }[] error: null } | { data: null error: StorageError } > { try { const data = await post( this.fetch, `${this.url}/object/sign/${this.bucketId}`, { expiresIn, paths }, { headers: this.headers } ) const downloadQueryParam = options?.download ? `&download=${options.download === true ? '' : options.download}` : '' return { data: data.map((datum: { signedURL: string }) => ({ ...datum, signedUrl: datum.signedURL ? encodeURI(`${this.url}${datum.signedURL}${downloadQueryParam}`) : null, })), error: null, } } catch (error) { if (isStorageError(error)) { return { data: null, error } } throw error } } 2. server action 파일에서 console.log는 개발자 도구가 아닌 터미널에 찍힌다는 사실..storageActions.ts에 정의한 uploadFile 함수를 수정하고 제대로 데이터가 들어가는지 확인하려고 console.log를 사용아무리 해도 useQuery까지는 잘 로그가 찍히는데, uploadFile에 작성한 로그가 브라우저 개발자도구에 전혀 찍히지 않아 뭐가 문제인지 한참 헤맴코드 에디터 터미널에 찍히고 있었음.. ㅠㅠ3. 파일명에 한글이 포함될 경우 업로드 안되는 오류디스코드에 다른 러너분들이 공유해준 정보에 따르면 supabase storage는 AWS의 S3 스토리지와 호횐되어, 파일명도 AWS S3의 Safe charaters - 영문, 숫자, 일부 특수기호 만 허용한다고 함어떻게 저장할까?base64 인코딩을 통해 S3-safe한 이름으로 변경해 저장하면 업로드 가능⇒ 저장 및 다운로드, 이름 표시하는 코드에서 인코딩/디코딩 함수를 사용하게 변경 완료어떻게 검색할까?인코딩/디코딩된 값으로 검색 호환이 안됨⇒ db에 저장해야 함db에 저장하도록 변경했는데, 한글 초성만 검색됨..todo-list 검색 때와의 차이가 뭐길래 안되는거지? 3. 미션 - 파일의 마지막 수정(업로드) 시간 표시하기storage를 확인하면, Added on, Last modified 정보가 있음file이 어떤 형태인지 로그 찍어 확인file.updated_at 키에 저장된 값임을 확인, 이 값을 사용추가로 file.metadata에 파일 타입과 사이즈 등의 정보도 있어 그 정보들도 적절히 표시하도록 함 (ui는 supabase storage를 참고함)

웹 개발Next.jsSupabaseReact.jsFile

LC-02s

[인프런 워밍업 클럽 3기] 풀스택 스터디 2주차 미션 회고 발자국

학습 내용 요약 인프런 워밍업 클럽 3기 풀스택 스터디 2주차에는 Supabase Storage를 활용하는 방법을 다루었습니다. 인프런에서 발자국을 작성할 때 강의를 보지 않고도 강의 내용을 파악할 수 있을 만큼 자세한 내용을 작성하는 건 지양해 달라고 가이드한 만큼 강의에 대한 필기는 최소화 하고 회고 위주로 적어보겠습니다. Supabase Strorage 기능 간단 정리Supabase의 Storage 기능은 파일 저장을 위한 서비스로, AWS S3와 같은 오브젝트 스토리지 기능을 제공함. Next.js, React, Flutter, Node.js 등 다양한 환경에서 사용할 수 있으며, PostgreSQL 기반의 권한 관리(RLS)를 지원하는 것이 특징임. 1. 파일 저장 및 관리이미지, 동영상, 문서 등 다양한 파일 형식을 저장 가능파일 업로드, 다운로드, 삭제, 이동 등의 기능 제공버킷(Bucket) 단위로 파일을 관리2. 권한 및 보안 (RLS)Row Level Security (RLS): PostgreSQL과 동일한 방식으로 접근 권한을 설정 가능공개(Public) 및 비공개(Private) 버킷 지원JWT 기반 인증을 사용하여 사용자별 접근 제한 가능3. 파일 접근 방식퍼블릭 파일: 누구나 URL을 통해 접근 가능프라이빗 파일: 인증된 사용자만 접근 가능 (Signed URL 활용)서명된 URL (Signed URLs): 일정 시간 동안만 유효한 URL 생성 가능4. 폴더 및 파일 구조디렉토리(폴더) 개념 지원폴더 내에서 파일 정리 및 관리 가능5. API 및 SDK 지원Supabase Client SDK를 통해 간편한 파일 업로드 및 관리 가능RESTful API 제공 (HTTP Client를 사용하여 직접 호출 가능)6. 이미지 변환 및 최적화Supabase Storage Image Transformation 지원 (이미지 크기 조절, 포맷 변경 등)CDN을 통해 빠른 이미지 제공 가능  Dropbox 클론 미션 회고 풀스택 스터디 2주차 미션은 강의에서 진행하는 Next.js와 Supabase Storage를 활용한 Dropbox 클론 앱에 사진 별 마지막 수정 시간을 표시하는 것이었지만, 저는 기타 편의기능을 더 추가해 보았습니다. 진행했던 미션은 해당 링크에서 보실 수 있습니다.  미션 수행 내용아래는 제가 수행한 미션에 대한 내용을 정리해보았습니다. 기능 명세이미지 파일 업로드 기능드래그 앤 드롭 기능다중 업로드 기능업로드한 이미지 파일 조회 기능키워드 검색 기능이미지 파일 다운로드 기능업로드한 이미지 파일 수정 기능이미지 파일명 변경 기능업로드한 이미지 파일 삭제 기능 강의에서는 기본적으로 파일 입출력에 관련된 기능만 다루었지만, 저는 실제 사용성을 고려하여 파일 업로드 전 미리보기 기능, 이미지 다운로드 기능, 파일 이름 변경 기능 등을 추가해 보았습니다. 또한 강의에서는 react-dropzone 라이브러리를 사용해서 드래그 앤 드롭 기능을 구현하였지만, 저는 프로젝트의 기본적인 스타일 프레임워크로 만타인을 사용하고 있었기 때문에, 만타인에서 따로 지원하는 @mantine/dropzone 라이브러리를 사용하여 구현하였습니다. 미션에 사용한 기술들은 아래에 따로 정리해 두었습니다. 사용할 때마다 느끼는 거지만 만타인은 정말 편한 것 같아요. 사용 기술프레임워크: Next.js v15, React v19데이터베이스: Supabase서버 상태관리: Tanstack Query v5클라이언트 상태관리: Zustand v5스타일 프레임워크: TailWindCSS v3, Mantine v7, Tabler Icons모노레포: Turbo Repo패키지 매니저: pnpm   트러블 슈팅아래는 미션을 진행하면서 만났던 문제들을 해결하는 과정에 대한 내용입니다. 파일명에 한글이 포함될 경우 Supabase Storage에 업로드하지 못하는 문제이미지 파일 이름에 한글이 포함될 경우 업로드가 되지 않는 문제가 있었습니다. 이슈를 찾아보니 Supabase Storage의 정책적인 문제였고, AWS의 S3 서비스도 동일한 문제를 가지고 있었기에 아래 조치들을 취하였습니다. 조치 1.처음 취했던 조치는 아래와 같이 nanoid 라이브러리를 활용하여 중복되지 않는 이름을 생성 후 기존의 파일 이름을 대체하는 방식을 사용했었습니다. 하지만 해당 방식을 사용하면 기존의 파일 이름이 사용자가 식별하지 못하는 값으로 대체되는 문제와, 중복되는 파일을 확인할 수 있는 방법이 없어지는 문제가 있어 최종적으로는 사용하지 않았습니다.'use server' import { nanoId } from 'nanoid' export const uploadImages = async ({ files, }: UploadImagesParams): Promise<{ data: { id: string; path: string } | null }[]> => { const client = await createServerSupabaseClient() return await Promise.all( files.map((file) => { const extension = extractExtension(file.name) const path = `/${nanoId() + '.' + extension}` return client.storage .from(process.env.SUPABASE_BUCKET_NAME!) .upload(path, file, { upsert: true }) }), ) } 조치 2.두 번째로 취한 조치는 조금 번거롭긴 하지만 파일과 1 대 1 로 대응되는 데이터베이스 테이블을 만들어서 관리하는 방식을 사용하였습니다. Supabase에서 지원하는 uuid를 활용하여 테이블의 Primary Key를 설정해 주었고, 이미지 업로드 시 먼저 테이블에 기존 파일 이름을 기반한 데이터 insert 후 생성된 uuid를 사용하여 파일명을 재설정하는 방식으로 우회하였습니다. Supabase에서 지원하는 uuid를 사용했기에 nanoid 같은 별도의 식별자 생성 라이브러리를 관리하지 않을 수 있었습니다.export const uploadImages = async ({ files, }: UploadImagesParams): Promise<{ data: { id: string; path: string } | null }[]> => { const client = await createServerSupabaseClient() const databaseQueries = files.flatMap((file) => { return client .from('minibox') .upsert({ name: file.name }) .select() .then((result) => result.data?.[0] ?? null) }) const targetFiles = await Promise.all(databaseQueries) const storageQueries = targetFiles.map((data) => { if (!data) { return { data: null } } const extension = extractExtension(data.name) const path = `/${data.id + '.' + extension}` const file = files.find((file) => file.name === data.name)! return client.storage .from(process.env.SUPABASE_BUCKET_NAME!) .upload(path, file, { upsert: false }) }) return await Promise.all(storageQueries) }업로드한 파일명이 한글일 경우 올바르게 검색 되지 않는 문제 (feat. MacOS)MacOS 환경에서 업로드한 파일을 별도의 후처리 없이 그대로 데이터베이스에 업로드 했더니 한글이 포함된 파일명에 대해서 아래와 같이 문자열 포함 여부를 판단하는 ilike 쿼리가 제대로 동작하지 않는 문제가 있었습니다.export const getImages = async ({ query = '' }: GetImagesParams): Promise<DroppedImageFile[]> => { const client = await createServerSupabaseClient() const imagesDataAll = await client.from('minibox').select('*').ilike('name', `%${query}}%`) } 원인을 분석해보니 아래와 같이 파일 이름에 한글이 포함되어 있을 시 자음과 모음이 모두 분리된 상태로 저장되어 있어 특정 키워드 포함 여부를 올바르게 판단하지 못해 발생한 문제였습니다.// input 'temp-훈이머리귤.jpeg'.split('')조치기존에 사용하던 ilike 쿼리를 제거하고, 자바스크립트에서 지원하는 String.prototype.normalize 메서드를 사용하여 기존 데이터에 대한 정규형 정준 결합(Normalization Form Canonical Composition) 절차를 거친 후 필터링을 거치는 방법으로 해결하였습니다.export const getImages = async ({ query = '' }: GetImagesParams): Promise<DroppedImageFile[]> => { const client = await createServerSupabaseClient() const imagesDataAll = await client.from('minibox').select('*') const targetData = imagesDataAll.data .filter(({ name }) => name.normalize('NFC').includes(query)) .map(({ id, name }) => `${id + '.' + extractExtension(name)}`) }  후기저는 이제껏 프론트엔드 개발을 접해보면서 한 번도 이미지 파일에 관련된 기능을 작업해보지 않았었습니다. 물론 서버 액션을 사용해서 조금 더 간략한 인터페이스를 사용했기에 실제 FormData 인터페이스를 사용하여 통신 로직을 작성하는 경험은 해보지 못했다는 한계가 있지만, 이번 미션을 통해 자바스크립트로 이미지 파일을 핸들링하는 방법과, Storage 서비스를 연동하여 저장까지 모두 접해볼 수 있어서 개인적으로는 만족스러웠던 한 주였던 것 같습니다. 긴글 읽어주셔서 감사합니다. ☺  

프론트엔드워밍업클럽3기풀스택Next.jsSupabase

정예은

[워밍업클럽3기] 백엔드 코드 - 박우빈 발자국 2주차

강의 수강 노션 링크https://www.notion.so/DAY06-1b2010f075ca80d09e08d4dd35376dd5?pvs=4https://www.notion.so/DAY07-1b3010f075ca80b4945cd230929481ff?pvs=4https://www.notion.so/DAY09-1b4010f075ca805f8e1ec01a7c00b16c?pvs=4https://www.notion.so/DAY10-1b4010f075ca80a7a731da0aa16b8e43?pvs=4출처[워밍업클럽 리더블코드 ][워밍업클럽 테스트코드] 👣발자국2주차👣🏫배운 내용 🏫주석의 양면성자주 변하는 정보는 최대한 주석 사용 지양하기우리가 가진 모든 표현방법을 총 동원해서 → 코드에 녹여 → 주석 사용 지지뢰찾기 리팩토링게임의 상태를 주석으로 설정하는 대신 → ENUM으로 관리그래서 외부에서 호출해서 사용하기무한루프 반복 구조는 위험특정 상황에서만 반복문 돌도록 바꿔줘야한다.지뢰찾기 같은 케이스에서는 게임이 “진행중” 일때만 반복문 돌 수 있도록 처리하기변수와 메서드의 나열 순서💡상태변경 메서드 >> 판별 메서드 >> 조회 메서드 순으로 나열 하자이때, 메서드 우선순위는 공개 메서드에서 private메서드 순으로 내려와야한다.  자동테스트?그동안 내가 학원에서 배워온 건, 수동테스트 였나보다스프링부트에서 애너테이션을 활용하여 수동사냥만 해왔던 것..인가?그리고 단위로 /unit으로 쪼개서 (메소드,클래스별로) 테스트를 진행 ⇒ 단위테스트그러다보니 검증속도도 빠르고 안정적임  JUnit이란?단위 테스트를 위한 프레임워크 → 퀜트백 프레임워크풍부한API제공해주는 프레임워크로 테스트 코드 작성해보자   JUnit vs assertJ 두개의 차이점AssertJ 의 장점자연어 가까워 가독성이 좋다체이닝 방식이 가능함JUni5의 단점assertEquals는 단순히 "Expected: A, Actual: B" 결과물만 추출함기능이 단순하고 제한적  assertJ 다양한 메서드기본적인 검증 isEqualTo() : 두 값이 같은지 비교 isNull() : 값이 null인지 비교 isTrue() : 값이 true인지 비교컬렉션 검증 hasSize() : 컬렉션의 크기 비교 —> 리스트의 사이즈 찾기 contains() : 컬렉션에 특정 요소가 포함되어 있는지 확인 isEmpty() : 컬렉션이 비어 있는지 확인 🎶경계값 조건 * 정수가 3이상일때, A라는 조건 만족해야함.해피케이스경계값 활용하기 !즉, 3에대한 테스트를 짜보자.5에 대해는 만족하지만, 3이 만족이 안될 수 있잖아 !예외 케이스2로 조건값보다 더 아래쪽 범위로 테스트 하기❗인사이트❗칭찬깃 사용법에 대해 좀더 연구하고, 프로젝트에 적용하며 강의를 따라가려고 노력하였다.깃에 대해 전혀 몰랐던 사람으로,,, 개발 공부하기 위해선 깃 활용이 무척이나 중요하다는 걸 깨달았다.코드리팩토링시 강의를 보며 , 강사님이 로직 처리를 하는 한단계 한단계씩 끊어서 정리하였다.테스트코드 진행시, 테스트하기 어려운 부분을(요구사항에 맞게 테스트 로직을 짰는데, 그 요구사항이 개발하는 시점의 요구사항이랑 충돌이 될때 )잘 이해하고 숙지하며 이 로직에 대해선 따로 분리하여 테스트 코드 관리하는 시야가 필요하다. 아쉬움그러나, 동영상 일시정지를 하고 노션에 정리한다고 한들, 온전히 내것이 되는가? 아쉬움이 남아있다.이상태로 다시 한번 해보세요~ 주어지면 , 아무것도 못한다.내 스스로 코드를 짜보는 학습이 필요할 것 같다.이번 미션11 코드 제출도 어디서 어떻게 시작 해야 할지 막막하다 앞으로 어떻게?지금도 지뢰찾기 코드 마스터 하지도 않고, 미션 제출도 선생님 코드 따라치기만 했었다. 지뢰찾기 로직을 파악하기엔 내 머리가 아직 준비가 안되었고, 내 마음의 여유가 준비되지 않은 상태였다. 현재 국비학원 졸업작품으로 팀프로젝트를 지난주에 시작 하다보니, 우선순위는 팀포폴이다. 그래서 시간을 내어 지뢰찾기 코드를 마스터 하기에는 조금은 어려울 듯 하여 ,,,, 팀포폴이 어느정도 마무리가 되어가면 그때 지뢰찾기 자바 코드 눈에 익히고 리팩토링 수업을 다시 들으며 공부를 해야 할 것 같다는 생각이 든다.   🧑🏻‍💻두번째 중간점검 나의 코드를 다른분 코드와 비교해보자 !미션4 미션 공통 피드백static정적 메소드는 빼자 ( 인텔리제이 단축키 사용한 사람 적.발) 풀스택 취업 준비백엔드의 매력은 ?눈에 예쁘게 보이는거 좋아하는데 → 프론트 개발자도 고민 → 프론트 앤드를 어느정도 잘 할줄 아는 백앤드 개발자가 되기로 함성향상 잘 맞을 것 같았다. 복잡한 방식을 여러 방법으로 접근 할 수 있는게 성향이 잘 맞았다. 따라치기만 하는 지금 상황어려움 보다는 익숙함의 문제이다 . 어려움건 10%일뿐 .익숙하지 않아서 거부감이 드는 것 일뿐,진짜 어려운건 아님 . 석박사 해야지 알수있는 정도는 아님메타인지 및 의도적으로 수련하는 것이 가장 빠르고 명확하다 💡될때까지 반복해라.💡계층구조 패키지 나누기 기준이란?도메인중심 ( 유저, 히스토리, 오더 )도메인별로 관심사가 명확해짐프로젝트가 커서, 도메인별로 떨어져야한다 → 아주 좋아유저가 회원이라는 도메인이 정말 중요해서 떼어야함 → 아주 유리 하다컨트롤러,서비스 계층들이 각각 저 도메인별로 나누어져 있다보니, 패턴이 달라질 수 있음공통기능이 멀리 떨어져 있으니, 공통기능이 중복으로 생성 될 우려가 있음 레이어 중심 ( 컨트롤러,서비스,모델)한눈에 레이어러 보기 좋아.도메인간 결합도가 증가해서 MSA전환이 불리하다 💡작은 프로젝트이면 레이어중심이 좋다💡큰 프로젝트는 도메인 중심으로 잡자. 개발 면접a 먼저 개발 지식 질문 CS기초, 스프링등b 이력서 기반 질문인성 질문 (개발에 대한 태도 )질문의 빈도는 A>B>C그러나 C가 별로이면 무조건 탈락취업준비에 대해회사가 원하는 기술들 JD가 무엇인지 공통적으로 찾고 있는 기술스택이 무엇인지 찾아보기그리고, 그 회사만이 찾고있는 기술,팀 도메인이 무엇인지 챙겨보기예상질문리스트 검색해서 → Interview Question Driven 취준 하기

백엔드박우빈워밍업클럽클린코드백엔드발자국

이재준

[한국산업기술협회 세미나 및 박람회] 스마트공장 SMT/PCB 불량유형별 분석대책 실무

SMT/PCB 품질 확보, 중요한 과제!!!불량 유형별 개선사례를 우리 기업에 적용 가능할까?다른 기업은 품질관리를 어떻게 하고 있을까? 최근 SMT 및 PCB 제조 공정에서다양한 불랑 사례가 지속적으로 보고 되고 있습니다!!!(솔더 브릿지, 부품미삽, 비아 홀 불량, 레이어 간 단락 등) 제조 공정서 발생하는 불량 줄이고, 신뢰성 높이는 것이기업 경쟁력의 핵심 요소!!! 이에 한국산업기술협회는SMT 및 PCB 최신 불량 유형 및 원인을 분석하고,효과적인 해결방안을 논의하며,생산성과 품질 향상 및 원가절감을 위한실질적인 인사이트를 제공하기 위해 세미나를 기획했습니다. 그리고 세미나에 참가하면"2025 스마트 SMT&PCB 어셈블리 박람회" 무료 참관 가능하다는 사실!!! 빠른 대안을 기획하고 운영하는 자만이미래를 선도할 수 있습니다. 많은 관심과 신청 부탁드립니다. 일시 및 장소 : 2025. 4. 3.(목), 10:00~17:00 / 수원컨벤션센터 106호사전등록 : 2025. 4. 2.(수) 까지참가비 : 200,000원 (사전등록) / 250,000원 (당일 현장 접수)참가비 할인 : 180,000원 (1업체 3인이상 접수시)신청 문의 : 02-6959-5562 / puma1708@kitanet.or.kr사전등록신청 : (구글폼) https://forms.gle/GmNgrLNAJa3sjDEYA세미나 등록 혜택 : "2025 스마트 SMT&PCB 어셈블리 박람회 무료 참관"기타 안내 : 교재, 강연파일, 다과, 중식 제공

반도체SMTPCB스마트SMTPCB어셈블리박람회세미나한국산업기술협회SMTPCB불량유형별분석대책실무품질확보품질관리원가절감

tikitaka

[인프런 워밍업 클럽 3기 풀스택] 2주차 발자국

[풀스택 완성] Supabase로 웹사이트 3개 클론하기 (Next.js 14) - 로펀 2주차는 Supabase Storage를 어떻게 사용하는지 학습했다. Supabase Storage 설정Supabase > DashBoard > Project > Storage버킷 생성하기Name of Bucket: 버킷 이름Public bucket: trueAdditional configuration>Allowed MIME types: image/* (이미지만 허용)Policies>For Full customizationPolicy name: 정책 이름Allowed operation: 누구나 CRUD를 할 수 있기 때문에 모두 허용Target roles: anon으로 누구나 허용Review 클릭>Save policy 클릭CRUD 각각에 대한 policies 생성server action에서 storage 접근하기export async function uploadFile(formData: FormData) { const supabase = await createServerSupabaseClient(); const files = Array.from(formData.entries()).map( ([name, file]) => file as File ); const results = await Promise.all( files.map((file) => supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .upload(file.name, file, { upsert: true }) ) ); return results; }Server Action을 사용하여 Supabase Storage에 파일을 업로드하는 함수이다. FormData를 받아 파일을 읽고, Supabase Storage에 업로드하는 방식으로 동작한다. const supabase = await createServerSupabaseClient();서버 측에서 Supabase 클라이언트를 생성하여 Storage API를 사용할 수 있도록 한다. const files = Array.from(formData.entries()).map( ([name, file]) => file as File );FormData에서 모든 항목을 배열로 추출하여 File 객체로 변환하여 파일을 추출한다.const results = await Promise.all( files.map((file) => supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .upload(file.name, file, { upsert: true }) ) ); return results;Promise.all()을 사용하여 여러 개의 파일을 동시에 업로드한다.supabase.storage.from(bucket).upload(filename, file, options)을 통해 지정한 버킷에 파일을 업로드한다.{ upsert: true }: 동일한 이름의 파일이 있을 경우 덮어쓴다. (insert + update) 업로드 된 결과를 반환한다. 2주차 미션github: https://github.com/thayoon/nextjs-supabase-dropbox-cloneDropbox Clone 프로젝트에 파일의 마지막 수정(업로드) 시간을 표시하는 기능을 추가하세요.파일 목록에서 각 파일의 “마지막 수정 시간”을 표시📌 참고 문서: Supabase Storage - 파일 목록 가져오기미션 해결 방법:list() 응답값 확인Supabase의 list() 함수를 사용하면 파일 정보를 가져올 수 있다.참고 문서에서 확인한 응답값은 다음과 같다:{ "data": [ { "name": "avatar1.png", "id": "e668cf7f-821b-4a2f-9dce-7dfa5dd1cfd2", "updated_at": "2024-05-22T23:06:05.580Z", "created_at": "2024-05-22T23:04:34.443Z", "last_accessed_at": "2024-05-22T23:04:34.443Z", "metadata": { "eTag": "\"c5e8c553235d9af30ef4f6e280790b92\"", "size": 32175, "mimetype": "image/png", "cacheControl": "max-age=3600", "lastModified": "2024-05-22T23:06:05.574Z", "contentLength": 32175, "httpStatusCode": 200 } } ], "error": null }이 중에서 updated_at이 파일의 마지막 수정 시간을 나타낸다. Server Action에서 list() 호출 및 데이터 반환actions/storageActions.tsexport async function searchFiles(search: string = "") { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .list(null, { sortBy: { column: "updated_at", order: "desc" }, search, }); handleError(error); return data; }파일 목록을 가져오는 searchFiles() 함수를 구현한다.참고 문서를 통해 sortBy 옵션을 적용하여 updated_at 을 기준으로 내림차순 정렬하여 최신 파일이 먼저 오도록 설정한다. 클라이언트 컴포넌트에서 데이터 가져오기components/dropbox-image-list.tsx "use client"; import { useQuery, useMutation } from "@tanstack/react-query"; export default function DropboxImageList({ searchInput }) { const searchImageQuery = useQuery({ queryKey: ["images", searchInput], queryFn: () => searchFiles(searchInput), }); return ( <section className="grid lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-2"> {searchImageQuery.isLoading && <Spinner />} {searchImageQuery.data && searchImageQuery.data.map((image) => ( <DropboxImage key={image.id} image={image} /> )} </section> ); } useQuery를 사용해 서버에서 데이터를 가져온다.가져온 데이터를 DropboxImage 컴포넌트로 전달한다. 마지막 수정 시간 표시components/dropbox-images.tsx"use client"; import { IconButton, Spinner, Checkbox } from "@material-tailwind/react"; import { getImageUrl } from "utils/supabase/storage"; export default function DropboxImage({ image }) { // 마지막 수정 시간 한국 시간 변환 const updated = new Date(image.updated_at) .toLocaleString("en-CA", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "Asia/Seoul", }) .replace(",", ""); return ( <div className="relative w-full flex flex-col gap-2 p-4 border border-gray-100 rounded-2xl shadow-md"> {/* Image */} {/* fileName */} {/* update time */} <p className="flex justify-end text-xs text-gray-500"> 마지막 수정: {updated} </p> {/* trash Button */} </div> ); } updated_at 값을 toLocaleString()을 사용해 한국 시간으로 변환하고 화면에 표시한다.추가 구현 사항긴 파일명 생략 표시<div className="truncate">{image.name}</div>className에 truncate를 적용하여 긴 파일명을 한 줄로 표시하고 넘칠 경우 "..."으로 생략한다. 사진 업로드 오름차순/내림차순 정렬처음에는 사용자의 정렬 방식 선택에 따라 서버에서 데이터를 다시 호출하도록 구현했지만, 비효율적이라고 판단하여 클라이언트에서 정렬을 처리하는 방식으로 변경했다.1차 시도 - 서버에서 정렬된 데이터 요청actions/storageAction.tsexport async function searchFiles(search: string = "", isLatest) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .list(null, { sortBy: { column: "updated_at", order: isLatest ? "desc" : "asc" }, search, }); handleError(error); return data; } isLatest 값에 따라 정렬 순서를 desc(최신순) 또는 asc(오래된순)으로 설정사용자가 정렬 방식을 변경할 때마다 서버 요청이 발생하여 비효율적이다.2차 시도 - 클라이언트에서 정렬 처리components/dropbox-image-list.tsx"use client"; import { Spinner, Menu, MenuHandler, MenuList, MenuItem, Button, Typography, } from "@material-tailwind/react"; import { useState } from "react"; import { useQuery } from "@tanstack/react-query"; const sortMenu = [ { title: "최신순", isLatest: true }, { title: "오래된순", isLatest: false }, ]; export default function DropboxImageList({ searchInput }) { const [openMenu, setOpenMenu] = useState(false); const [isLatest, setIsLatest] = useState(true); const searchImageQuery = useQuery({ queryKey: ["images", searchInput], queryFn: () => searchFiles(searchInput), }); return ( <div> {/* 정렬 버튼 */} <div className="flex flex-row-reverse"> <Menu open={openMenu} handler={setOpenMenu} allowHover> <MenuHandler> <Button variant="text" className="flex items-center gap-1 align-middle text-base font-normal capitalize tracking-normal" > {isLatest ? "최신순" : "오래된순"} <i className={`fas fa-angle-down transition-transform ${ openMenu ? "rotate-180" : "" }`} /> </Button> </MenuHandler> <MenuList className="hidden gap-3 overflow-visible lg:grid"> <ul className="flex w-full flex-col gap-1"> {sortMenu.map(({ title, isLatest }) => ( <MenuItem key={title} onClick={() => setIsLatest(isLatest)}> <Typography variant="h6" color="blue-gray" className="mb-1"> {title} </Typography> </MenuItem> ))} </ul> </MenuList> </Menu> </div> {/* 이미지 리스트 */} <section className="grid lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-2"> {searchImageQuery.isLoading && <Spinner />} {searchImageQuery.data && (isLatest ? searchImageQuery.data.map((image) => ( <DropboxImage key={image.id} image={image} /> )) : searchImageQuery.data .slice() .reverse() .map((image) => <DropboxImage key={image.id} image={image} />))} </section> </div> ); }서버 요청은 기본적으로 최신순으로 설정하고, 클라이언트에서 데이터를 reverse()하여 정렬을 변경하는 방식으로 개선했다. 사진 다중 삭제사진을 다중 선택하여 삭제하는 기능은 체크박스를 활용해 구현했다.사용자는 "전체 선택" 및 "선택 삭제" 기능을 통해 한 번에 여러 사진을 삭제할 수 있다.components/dropbox-image-list.tsx "use client"; export default function DropboxImageList({ searchInput }) { // ... const [allSelected, setAllSelected] = useState(false); const [isSelected, setIsSelected] = useState([]); const searchImageQuery = useQuery({ queryKey: ["images", searchInput], queryFn: () => searchFiles(searchInput), }); const deleteFileMutation = useMutation({ mutationFn: deleteFile, onSuccess: () => { searchImageQuery.refetch(); }, }); function handleChecked(isChecked) { setAllSelected(isChecked); if (isChecked && searchImageQuery.data) { setIsSelected(searchImageQuery.data.map((image) => image.name)); } else { setIsSelected([]); } } return ( <div role="section"> <div className="flex justify-between"> <div className="flex justify-center items-center gap-3"> <Checkbox color="blue" label={ <Typography> 전체 선택 ({isSelected.length}/ {searchImageQuery.data && searchImageQuery.data.length} {!searchImageQuery.data && 0}) </Typography> } checked={allSelected} onChange={(e) => handleChecked(e.target.checked)} /> <Button className="rounded-full" size="sm" variant="outlined" color={isSelected.length > 0 ? "blue" : "gray"} disabled={isSelected.length > 0 ? false : true} onClick={() => { setIsSelected([]); setAllSelected(false); deleteFileMutation.mutate(isSelected); }} > {deleteFileMutation.isPending ? <Spinner /> : "선택 삭제"} </Button> </div> // ... </div> <section className="grid lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-2"> {searchImageQuery.isLoading && <Spinner />} {searchImageQuery.data && searchImageQuery.data.map((image) => ( <DropboxImage key={image.id} image={image} isSelected={isSelected} setIsSelected={setIsSelected} setAllSelected={setAllSelected} totalLength={searchImageQuery.data.length} /> ))} </section> </div> ); } 전체 선택: 사용자가 "전체 선택" 체크박스를 클릭하면, 모든 이미지가 선택된다.선택된 이미지 수와 총 이미지 수가 표시된다.선택 삭제: 사용자가 선택한 이미지들을 삭제할 수 있는 "선택 삭제" 버튼을 제공한다. 이미지가 선택되었을 때만 활성화된다.상태 관리: isSelected 배열에 선택된 이미지의 이름을 저장한다. allSelected 상태로 전체 선택 여부를 관리한다.선택 삭제 버튼을 클릭하면 deleteFileMutation을 호출하여 isSelected을 전달하여 삭제를 처리한다.삭제 작업이 완료되면 searchImageQuery.refetch()를 호출하여 이미지 리스트를 최신 상태로 갱신한다.components/dropbox-images.tsx"use client"; export default function DropboxImage({ image, isSelected, setIsSelected, setAllSelected, totalLength, }) { const isChecked = isSelected.includes(image.name); const handleChecked = (checked) => { setIsSelected((prev) => { if (checked) { const newSelected = [...prev, image.name]; if (newSelected.length === totalLength) setAllSelected(true); return newSelected; } else { setAllSelected(false); return prev.filter((item) => item !== image.name); } }); }; // ... return ( <div className="relative w-full flex flex-col gap-2 p-4 border border-gray-100 rounded-2xl shadow-md"> {/* Image */} {/* fileName */} {/* update time */} {/* multiple checkBox */} <div className="absolute top-4 left-4"> <Checkbox color="blue" className="border-2 border-white bg-white/30 checked:border-white checked:bg-blue-500" checked={isChecked} onChange={(e) => handleChecked(e.target.checked)} /> </div> {/* trash Button */} </div> ); } 개별 체크박스: 각 이미지에 대해 체크박스를 제공하고 사용자가 선택한 이미지를 isSelected 배열에 추가하거나 제거한다.상태 변화: 체크박스를 클릭하면 해당 이미지가 선택되거나 선택이 해제되고 선택된 모든 이미지가 삭제될 때 "전체 선택" 체크박스도 자동으로 갱신된다. 상태 연동: isChecked 개별 이미지에 체크박스의 체크 여부를 결정한다.isSelected 배열에 현재 이미지의 name 값이 포함되어 있는지 true, false로 설정한다.개별 이미지의 체크 여부가 isSelected 상태와 동기화된다.actions/storageActions.tsexport async function deleteFile(fileName: string[]) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .remove(fileName); handleError(error); return data; } supabase.storage.from(bucket).remove(['filename'])을 통해 지정한 버킷에서 배열에 포함된 모든 파일을 삭제 요청한다. 2주차 회고☀이번주는 강의를 들으며 여러 추가 기능이 생각나 실습하는 과정이 더욱 즐거웠다.다른 러너분이 한글 파일명 업로드 오류를 찾아내고 해결하는 모습을 보고 대단하다고 느꼈다.실습에서 영어 파일명으로만 업로드 했기 때문에 이런 오류가 발생하는지 몰랐다.상황을 공유해주신 러너분 덕분에 새로운 사실을 알게 됐다. (감사합니다!)다음에 시간이 난다면 강사님께서 말씀하신 해결 방법을 적용해 보고 싶다.목요일에 중간 점검 시간에 QnA 시간을 가졌는데, 정말 도움이 많이 됐다.특히, 포트폴리오 작성 요령과 개발자로서 필요한 역량을 채우는 방법을 핵심적으로 짚어주셔서 큰 도움이 되었다.나만의 특색을 찾고 포트폴리오에 잘 정리해 봐야겠다 느꼈다.벌써 2주차가 끝났는데, 배포까지 빠르게 진행해 보고 싶다. 다음주도 화이팅! 

치현

[인프런 워밍업 스터디 클럽 3기 풀스택] 2주차 발자국

학습 내용인프런 워밍업 클럽 스터디 2주차로, 이번 주는 드롭 박스 프로젝트와 함께 Supabase의 Storage를 다뤄볼 수 있는 시간이었다. Supbase Storage1. 기본 구성 요소Files: 모든 종류의 미디어 파일 저장 가능 (이미지, GIF, 비디오 등)Folders: 파일을 체계적으로 구성하기 위한 디렉토리 구조Buckets: 파일과 폴더를 담는 최상위 컨테이너 (접근 규칙별로 구분)2. 접근 제어 모델Private Buckets (기본값) RLS(Row Level Security) 정책을 통한 접근 제어JWT 인증 필요Signed URL을 통한 임시 접근 가능Public Buckets파일 조회 시 접근 제어 없음URL만 있으면 누구나 접근 가능업로드/삭제 등 다른 작업은 여전히 접근 제어 적용3. 보안 기능RLS 정책 설정 가능SELECT (다운로드)INSERT (업로드)UPDATE (수정)DELETE (삭제)소유권 관리owner_id 필드로 리소스 소유자 추적JWT의 sub claim 기반 소유권 할당4. 이미지 변환 기능 (Pro Plan 이상)실시간 이미지 최적화크기 조정품질 조정 (20-100)WebP 자동 최적화변환 옵션resize 모드: cover, contain, fillwidth/height 지정 (1-2500px)최대 파일 크기: 25MB최대 해상도: 50MP5. 인증 방식S3 액세스 키서버 사이드 전용모든 버킷에 대한 완전한 접근 권한세션 토큰클라이언트 사이드 사용 가능RLS 정책 기반 제한된 접근6. 통합 기능Next.js 이미지 로더 지원AWS S3 호환성PostgreSQL DB와 연동7. 제한사항파일명은 AWS S3 명명 규칙 준수 필요HTML 파일은 보안상 plain text로 반환이미지 변환 기능은 Pro Plan 이상에서만 사용 가능미션 2 구현 내용과제 구현 저장소Dropbox 중파일의 마지막 수정(업로드) 시간을 표시하는 기능 추가 하기 export interface FileObject { name: string bucket_id: string owner: string id: string updated_at: string created_at: string last_accessed_at: string metadata: Record<string, any> buckets: Bucket }=> DropboxImage 컴포넌트가 prop으로 받는 image의 타입은 FileObject로 그 중 업로드시간은 created_at을 의미하기에 이를 이미지에 추가하였다.(사진 참고) 포인트 1: 한글 파일명 es-hangul 사용// 안전한 파일명 생성을 위한 유틸리티 export class FileNameConverter { // 안전한 문자 패턴 정의 private static readonly SAFE_CHARACTERS = /^[a-zA-Z0-9!\-_.*'()]+$/; private static generateRandomString(length: number = 8): string { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; return Array.from({ length }, () => chars.charAt(Math.floor(Math.random() * chars.length)) ).join(""); } // 파일명이 안전한 문자들로만 구성되었는지 확인 private static isSafeFileName(name: string): boolean { return this.SAFE_CHARACTERS.test(name); } // 안전하지 않은 문자를 포함한 파일명을 안전한 형식으로 변환 private static convertToSafeFileName(name: string): string { try { // 파일명 정규화 const normalized = name.trim().normalize(); // 한글이나 특수문자가 있는지 확인 const hasKorean = /[ㄱ-ㅎㅏ-ㅣ가-힣]/.test(normalized); const hasSpecialChars = /[^A-Za-z0-9]/.test(normalized); if (!hasKorean && !hasSpecialChars) { return normalized; } // 한글이 있는 경우 로마자로 변환 시도 if (hasKorean) { const romanized = romanize(normalized); if (romanized && romanized !== normalized) { // 로마자 변환 결과에서 안전하지 않은 문자 제거 return romanized.replace(/[^a-zA-Z0-9]/g, "_").toLowerCase(); } } // 변환 실패 시 랜덤 문자열 생성 return this.generateRandomString(); } catch (error) { console.error("Conversion error:", error); return this.generateRandomString(); } } // 원본 파일명을 안전한 형식으로 변환 static encode(fileName: string): string { console.log("Original filename:", fileName); const extension = fileName.split(".").pop() || ""; const nameWithoutExt = fileName.slice(0, fileName.lastIndexOf(".")); const safeName = this.isSafeFileName(nameWithoutExt) ? nameWithoutExt : this.convertToSafeFileName(nameWithoutExt); console.log("Safe filename:", safeName); return `${safeName}_${Date.now()}.${extension}`; } // 파일명에서 타임스탬프 제거하여 원본 이름 추출 static decode(fileName: string): string { const [name] = fileName.split("_"); return name || fileName; } }포인트 2 : 업로드 날짜 표시export function formatDate(timestamp: string): string { const date = new Date(timestamp); const now = new Date(); const diff = now.getTime() - date.getTime(); // 1일 이내 if (diff < 24 * 60 * 60 * 1000) { const hours = Math.floor(diff / (60 * 60 * 1000)); if (hours < 1) { const minutes = Math.floor(diff / (60 * 1000)); return `${minutes}분 전`; } return `${hours}시간 전`; } // 30일 이내 if (diff < 30 * 24 * 60 * 60 * 1000) { const days = Math.floor(diff / (24 * 60 * 60 * 1000)); return `${days}일 전`; } // 그 외 return date.toLocaleDateString("ko-KR", { year: "numeric", month: "long", day: "numeric", }); } 회고파일명 변환하는데 생각보다 시간이 많이 소요됐다.여찌저찌 구현은 헀지만, 이미지가 어떻게 encoding되고 decoding되는지 일련의 과정에 대한 공부가 필요함을 느끼는 이번주 였다.  

풀스택풀스택인프런워밍업스터디클럽Next3기SupabaseReact프론트엔드2주차발자국

Masocampus

[GEN AI 인사이트] AI도 실수한다? 그래서 등장한 CriticGPT!

AI는 완벽할까요? 사실 그렇지 않아요! AI도 실수를 하고, 오류를 범할 수 있죠. 🤖그래서 나온 것이 CriticGPT입니다! AI가 생성한 코드 속 오류를 찾아내는 AI, CriticGPT를 함께 알아볼까요?CriticGPT는 GPT-4 기반 AI로, ChatGPT가 생성한 코드에서 오류를 찾아내는 역할을 해요.AI가 스스로의 실수를 바로잡는 시대, 신기하지 않나요? 😃AI의 답변이 점점 더 정교해질수록, 그 안에 숨은 오류를 찾는 일이 더 어려워지고 있어요.✅ ChatGPT의 답변이 점점 더 정확해지면서 오류 검출이 복잡해짐✅ 강화 학습 과정에서 AI 트레이너의 부담 증가💡 그래서 등장한 CriticGPT! AI의 답변을 분석하고 오류를 지적하도록 학습되었어요.기존에는 사람이 직접 AI의 오류를 찾아야 했어요. 하지만 실수 가능성이 있었죠.❌ 일반 검토 방식 → 사람이 직접 오류를 찾음 → 실수 가능성 증가⭕ CriticGPT 활용 → 보다 철저한 검토 가능 → 불필요한 오류 지적 감소👉 AI 트레이너가 CriticGPT를 활용하면, 보다 정확하고 신뢰도 높은 피드백을 작성할 수 있어요!CriticGPT는 단순한 AI가 아니에요! 꾸준한 학습을 통해 점점 더 정교한 오류 탐지 능력을 갖추고 있어요. AI 트레이너가 일부러 코드에 버그 삽입 CriticGPT가 오류를 찾아내고 비평 작성 여러 비평을 비교하며 정확성 높은 피드백을 학습 이를 반복하며 더 정밀한 오류 탐지 능력을 갖춤이렇게 꾸준한 학습을 통해 AI의 신뢰도를 높이는 역할을 하고 있어요! 😊🔹 Step 1: ChatGPT가 생성한 코드 샘플 선택🔹 Step 2: 코드에 버그 삽입 → CriticGPT가 오류를 찾아내도록 학습🔹 Step 3: 여러 비평을 생성 후 분석 → 오류 검출률, 정확성, 신뢰도를 평가👉 이 과정을 반복하면서 CriticGPT는 점점 더 똑똑해져요! 🚀CriticGPT도 완벽한 것은 아니에요. 몇 가지 한계가 존재하죠.⚠ 짧은 코드 위주로 학습됨 → 긴 코드 분석 능력 부족⚠ 환각 현상(hallucination) 발생 → 존재하지 않는 오류를 생성할 가능성⚠ 복잡한 문제 평가 어려움 → AI가 정확하게 평가하지 못할 수도 있음하지만 이러한 한계를 극복하기 위한 추가 연구가 진행 중이에요! 앞으로 더 발전할 CriticGPT, 기대되지 않나요? 😃💡 CriticGPT 덕분에 AI의 신뢰도가 더 높아지고 있어요!✅ AI 검토 AI의 등장 → CriticGPT가 AI 오류를 잡아냄✅ 정확한 피드백 제공 → AI 트레이너의 부담 완화✅ 더 발전하는 AI → CriticGPT 덕분에 더욱 신뢰할 수 있는 AI 시대AI의 가능성을 깨우는 마소캠퍼스와 함께, 더 스마트한 AI 활용법을 배워보세요! 😊마소캠퍼스와 함께 AI를 활용해 업무 혁신을 이뤄보세요! 효율적이고 스마트한 일의 방식을 통해 성장할 수 있도록 도와드릴게요. 📌 관련 강의 <ChatGPT 최신 모델 프롬프트 엔지니어링 바이블>실전형 프롬프트 엔지니어링을 익히고, AI 챗봇(ChatGPT)을 활용해 성과 극대화!

AI 업무 활용aiai도구criticgptai오류인공지능ai검토ai학습마소캠퍼스ai활용gpt

김경환

워밍업 클럽 3기 BE 클린코드&테스트 - 1주차 발자국

회고저는 그동안 제가 이상적인 코드를 작성하고 있다고 생각했습니다. 나름 많은 고민을 코드에 녹여냈고 가독성을 항상 신경썼습니다. 하지만 이번 강의를 통해서 제가 많은 부분들을 몰랐다는 점에서 놀랐습니다. 섹션 3을 통해 전반적인 안 좋은 습관들을 교정해나갈 기회는 저에게 값집니다. 남은 시간들도 기대가 됩니다. 강의 내용 요약인트로우리는 왜 이 강의를 듣는가우리는 코드를 읽는 시간을 코드를 쓰는 시간보다 더 많이 할애한다우리가 읽어야 하는 코드 :여러 사람이 작성한 코드내가 한시간 전에 작성한 코드읽기 좋은 코드는 더 나은 코드 작성을 위해 필수적이다 코드를 잘 짠다는 것은?읽기 좋은 코드를 작성하는 것"코드는 작성한 순간부터 레거시다."코드의 독자 :미래의 동료미래의 나읽기 어려운 코드는 추후의 모두에게 악영향을 미친다이 강의에서는 읽기 좋은 코드를 위해 어떤 관점으로 어떻게 접근해야 좋을지 이야기한다 추상우리가 클린 코드를 추구하는 이유클린 코드를 추구함으로써 가독성을 확보할 수 있다가독성이 높으면 글이 잘 읽힌다= 이해가 잘된다가독성이 높으면 코드가 잘 읽힌다= 이해가 잘된다= 유지보수하기가 수월하다= 우리의 시간과 자원이 절약된다.클린 코드를 작성하기 위해 우리는 추상화에 집중해야 한다  추상과 구체추상이란?어떤 모습에서 형상을 뽑아내는 것구체적인 정보에서 어떤 이미지를 뽑아내는 것특정한 측면만을 가려내어 포착하는 것특정한 측면 외 나머지는 버린다는 것중요한 정보는 남기고, 덜 중요한 정보는 생략하여 버린다. 추상화 레벨추상화 정도에 따라 레벨이 나뉜다추상화 레벨이 높을 수록 중요한 부분만 남기고 나머지는 제한다. 추상화의 가장 대표적인 행위이름 짓기 이름 짓기이름 짓기프로그래머가 가장 힘들어하는 일이름을 짓는다는 행위는 추상적 사고를 기반으로 한다.추상적 사고표현하고자 하는 구체에서 정말 중요한 핵심 개념만을 추출하여 잘 드러내는 표현우리 도메인의 문맥 만에서 이해되는 용어 이름 짓기 유의 사항단수와 복수 구분하기말미에 '-(e)s'를 붙여 어떤 데이터가 단수인지 복수인지를 명확히 하는 것만으로도 읽는 이에게 중요한 정보를 같이 전달할 수 있다.이름 줄이지 않기줄임말이라는 것은 가독성을 제물로 바쳐 효율성을 확보하는 것보통 이름을 줄임으로써 얻는 것보다 잃는 것이 많아 자제하는 것이 좋다다만 관용어처럼 많은 사람들이 자주 사용하는 줄임말이 있다.이런 줄임말이 이해될 수 있는 바탕은 문맥에 있다. 은어/방언 사용하지 않기특정 집단에서만 이해될 수 있는 은어 사용 금지기준 : 새로운 사람이 팀에 합류했을 때 이 용어를 단번에 이해할 수 있는가?도메인 용어 사용하기이 경우 도메인 용어를 먼저 정의하는 과정 (ex.도메인 용어 사전)이 선행되어야 할 수 있다이상적인 표현을 좋은 코드들을 통해 습득하기비슷한 상황에서 자주 사용하는 단어, 개념 습득하기ex. pool, candidate, threshold 등 메서드와 추상화한 문단의 주제는 반드시 하나다잘 쓰여진 코드 또한 하나의 주제만을 가진다생략할 정보와 의미를 정하고 드러낼 정보를 구분해야 한다.메서드 선언부메서드명추상화된 구체를 유추할 수 있는, 적절한 의미가 담긴 이름파라미터와 연결 지어 더 풍부한 의미를 전달할 수도 있다.파라미터파라미터의 타입, 개수, 순서를 통해 의미를 전달파라미터는 외부 세계와 소통하는 창반환 타입메서드 시그니처에 납득이 가는, 적절한 타입의 반환값 돌려주기메서드의 반환 타입만 보고도 바로 이해가 되어야 한다. void 대신 충분히 반환할만한 값이 있는지 고민해보기void로 충분할 경우도 있지만 가급적 반환값 사용하기반환값을 둘 경우 테스트도 용이해진다. 추상화 레벨하나의 세계 안에서는, 추상화 레벨이 동등해야 한다. 매직 넘버, 매직 스트링상수를 추출한다는 것의 의미이름을 추출한다는 것은 그 자체로 추상화상수도 이와 같다매직 넘버, 매직 스트링이란?의미를 갖고 있으나, 상수로 추출되지 않은 숫자, 문자열 등상수 추출로 이름을 짓고 의미를 부여함으로써 다음의 이점들을 확보할 수 있다.가독성유지보수성 뇌 메모리 적게 쓰기멀티 태스킹은 곧 저글링을 하는 것과 다름이 없다 (책 '도둑맞은 집중력' 발췌)사람은 한번에 하나의 일에만 집중할 수 있다.또한 하던 일을 다른 일로 전환할 경우 그에 따른 전환 비용이 발생한다.우리가 읽기 가장 좋은 코드는 한번에 읽히는 코드이다.이를 위해 아래 세가지 요소는 지양해야 한다.이해하려면 기억해야 하는 정보낮은 추상화 레벨불필요한 정보 Early return조건문을 사용할 때 else if 나 else를 사용할 경우사용자는 else의 선행 조건을 파악하기 위해 앞선 if와 else if들을 확인해야 한다.이는 사용자가 이해를 위해 기억력을 할당해야 하는 안 좋은 패턴이다.else if 와 else 는 지양해야 한다.가급적 지양코드가 짧은 등의 이유로 가독성의 문제가 없을 경우는 예외이다.같은 이치로 switch문도 가급적 지양해야 한다.  사고의 depth 줄이기중첩 분기문, 중첩 반복문중첩되는 분기문과 반복문은 함수로 따로 빼는 것이 좋을 수 있다.함수로 분리함으로써 읽는 사람으로 하여금 사고의 깊이를 줄여 가독성을 높여준다.다만 간단한 중첩문, 분기문의 경우 오히려 분리하지 않는게 좋다.사용할 변수는 가까이 선언사용된 변수가 20줄이 넘어가는 이전의 코드에서 선언될 경우읽는 입장에선 해당 변수의 존재를 확인하기 위해 다시 20줄 위로 올라가야 한다.변수 사용부와 선언부를 가까이 하여 가독성을 높이자 공백 라인을 대하는 자세공백 라인도 의미를 가진다.복잡한 로직의 의미 단위를 나누어 읽는 사람에게 추가적인 정보를 제공할 수 있다. 부정어를 대하는 자세부정 연산자의 경우 가독성이 떨어진다독자로 하여금 사고의 반전을 강제한다.부정어 대처법부정어구를 쓰지 않아도 되는 상황인지 체크부정의 의미를 담은 다른 단어가 존재하는지 고민부정어구로 메서드명 구성 해피 케이스와 예외 처리예외를 대하는 자세예외가 발생할 가능성 낮추기어떤 값의 검증이 필요한 부분은 주로 외부 세계와의 접점인 점에 유의하기ex. 사용자 입력, 객체 생성자, 외부 서버의 요청 등의도한 예외와 예상하지 못한 예외를 구분하기사용자에게 보여줄 예외와 개발자가 직접 보고 처리해야 할 예외 구분Null을 대하는 자세항상 NullPointException을 방지하는 방향으로 경각심 가지기메서드 설계 시 return null 자제하기만약 어렵다면 Optional 사용을 고려Optional을 대하는 자세Optional은 비싼 객체꼭 필요한 상황에서만 활용Optional을 파라미터로 받지 않도록 한다이 경우 분기 케이스가 세가지나 된다Null인 경우Null이 아닌 경우Optional 자체가 Null인 경우Optional을 반환 받았다면 최대한 빠르게 해소한다Optional을 해소하는 방법분기문을 만드는 isPresent()-get() 대신 풍부한 Optional의 API 사용ex. orElseGet(), orElseThrow(), ifPresent(), ifPresentOrElse()orElse(), orElseGet(), orElseThrow()의 차이를 숙지해야 한다orElse() : 항상 실행, 확정된 값일 때 사용orElseGet() : null인 경우 실행, 값을 제공하는 동작 정의 추상의 관점으로 바라보는 객체 지향객체란?추상화된 [데이터 + 코드]관심사의 분리특정한 관심사에 따라서 객체를 만들어낼 수 있다.관심사에 따라 기능과 책임을 나눈다나눈 관심사를 바탕으로 어플을 만든다.이를 통해 유지보수성을 높일 수 있다.높은 응집도, 낮은 결합도특정한 관심사끼리 응집도가 높아야 한다관심사 내의 기능들 간의 결합도가 낮아야 한다뜻A를 수정했을 때 B가 큰 영향을 받아선 안된다. 객체 설계하기객체로 추상화하기사용자는 객체의 내부 로직을 알 필요가 없다 공개 메서드 선언부를 통해 외부 세계와 소통하고 나머지 필드나 로직들은 비공개를 함으로써 캡슐화한다.객체의 책임을 나눔으로써 객체 간 협력을 유도한다.객체가 제공하는 것절차 지향에서 잘 보이지 않았던 개념의 가시화관심사를 한 군데에 모음으로써 높은 응집도 확보= 유지보수성 증가객체를 사용하는 입장에선 구체적인 내부 구현을 신경쓰지 않고 높은 추상화 레벨의 도메인 로직을 다룰 수 있다.새로운 객체를 만들 때 주의할 점1개의 관심사로 명확하게 책임이 정의되어 있는 지 확인하기 setter 사용 자제객체 내부에서 외부 세계의 개입 없는 방식을 추구함으로써 의도치 않은 버그를 사전에 방지사용이 필연적일 경우 'set~'이라는 이름 대신 'update~'와 같은 더 명확한 이름을 고려getter 사용 자제setter 와 같은 이치로 직접 꺼내서 사용하기보다 가급적 객체 내에서 해결하게끔 설계getter의 경우 setter와는 달리 필요할 경우 사용해도 된다.필드의 수 최소화불필요한 데이터가 늘수록 복잡도가 올라 유지보수성이 낮아진다.기존의 필드들을 통해 계산할 수 있는 기능들은 메서드를 통해 제공하자.단, 미리 계산하는 것이 성능상의 이점을 가질 경우 필드로 사용 가능 SOLIDSRP단일 책임 원칙 (Single Responsibility Principle)하나의 클래스는 단 한가지의 변경 사유만을 가져야 한다변경 사유 = 책임 = 관심사SRP 원칙을 잘 지킬 경우의 이점 :관심사의 분리높은 응집도낮은 결합도OCP개방-폐쇄 원칙 (Open-Closed Principle)확장에는 열려있고 수정에는 닫혀 있어야 한다.기존 코드의 변경 없이, 시스템의 기능을 확장할 수 있어야 한다.필수 요소 :추상화다형성LSP리스코프 치환 원칙 (Liskov Substitution Principle상속 구조에서, 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 치환할 수 있어야 한다.자식 클래스는 부모 클래스의 책임을 준수하고부모 클래스의 행동을 변경하지 않아야 한다LSP를 위반할 경우의 문제점 :어플리케이션 오동작예상 밖의 예외위 두 문제를 방지하기 위한 불필요한 타입 체크 동반ISP인터페이스 분리 원칙 (Interface Segregation Principle)클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안된다.인터페이스를 잘게 쪼개자.기능 단위로 인터페이스를 나눠서 사용하자.ISP를 위반할 경우의 문제점 :불필요한 의존성으로 인한 결합도 상승DIP의존성 역전 원칙 (Dependency Inversion Principle)상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다.추상화에 의존해야 한다.의존성의 순방향 : 고수준 모듈이 저수준 모듈을 참조의존성의 역방향 : 고수준, 저수준 모듈이 모두 추상화(인터페이스, 추상 클래스)에 의존DIP를 잘 지킬 경우 저수준 모듈이 변경되어도 고수준 모듈에는 영향이 가지 않는다. 상속과 조합상속보다는 조합을 사용해야 한다.상속은 시멘트처럼 굳어지는 구조다.상속의 단점수정이 어려움부모와 자식 간의 결합도가 높음부모가 수정될 경우 모든 자식들에게 영향조합과 인터페이스를 활용하여 유연한 구조를 얻을 수 있다.상속을 통한 코드 중복 제거가 주는 이점보다 중복이 생기더라도 유연한 구조 설계가 가능한 조합이 주는 이점이 더 크다. Value Object도메인의 어떤 개념을 추상화하여 표현한 값 객체값으로 취급하기 위해서 아래 세 가지 요소를 보장해야 한다.불변성final 필드 사용setter 금지동등성서로 다른 인스턴스여도(=동일성이 달라도), 내부의 값이 같으면 같은 값 객체로 취급equals() & hashCode() 재정의 필요유효성 검증객체가 생성되는 시점에 값에 대한 유효성 보장VO vs EntityVO와 Entity의 가장 큰 차이점은 식별자 유무이다Entity식별자가 있다식별자만 같으면 다른 필드가 달라도 동등한 객체로 취급식별자가 다르지만 필드가 다를 경우 시간이 지남에 따라 변화한 것으로 취급VO식별자가 없다내부의 모든 값이 다 같아야 동등한 객체로 취급이는 곧, 전체 필드가 식별자 역할을 한다고 볼 수 있다. 일급 컬렉션컬렉션을 포장하면서 컬렉션만을 유일하게 필드로 가지는 객체컬렉션을 다른 객체와 동등한 레벨로 다루기 위해 사용한다단 하나의 컬렉션 필드만을 가진다컬렉션을 추상화하여 의미를 담을 수 있고, 가공 로직의 보금자리가 생긴다.가공 로직에 대한 테스트도 작성할 수 있다.만약 컬렉션을 반환해야 할 경우 새로운 컬렉션을 반환해야 한다.기존의 컬렉션을 변경할 여지를 없앤다. Enum의 특성과 활용Enum은 상수의 집합상수와 관련된 로직을 담을 수 있는 공간상태와 행위를 한 곳에서 관리할 수 있는 추상화된 객체특정 도메인 개념에 대해 그 종류와 기능을 명시적 표현 가능만약 변경이 잦은 개념은 Enum보다 DB로 관리하는 것이 나을 수 있다. 숨겨져 있는 도메인 개념 도출하기도메인 지식은 만드는 것이 아니라 발견하는 것객체 지향은 현실을 100% 반영하는 것이 아닌 흉내내는 것이다.이를 통해 현실 세계에서 쉽게 인지하지 못하는 개념도 도출해서 사용할 수 있다.완벽한 설계라는 것은 불가능하다근시적, 거시적 관점에서 최대한 미래를 예측해야 한다시간이 지나 만약 틀렸다는 것을 인지할 경우를 상정하고 코드를 작성해야 한다.미션Day 2미션 설명"추상과 구체"의 강의를 듣고 생각나는 추상과 구체의 예시가 있다면 한번 3~5문장 정도로 적어봅시다. 일상 생활, 자연 현상, 혹은 알고 있는 개발 지식 등 어느 것이든 상관 없습니다. 추상에서 구체로, 또는 구체에서 추상으로 방향은 상관 없으나, 어떤 것이 추상이고 어떤 것이 구체 레벨인지 잘 드러나게 작성해 보아요:) 나의 답1.추상코드를 꼽는다구체기기에 연결되어 있는 코드를 콘센트의 두 구멍에 맞춰 연결함으로써 전력을 공급한다.2.추상커피를 마신다구체커피가 담긴 컵을 손으로 잡아 고정시킨 상태에서 컵의 각도를 조절하여 커피를 입에 주입한다.3.추상지하철을 탄다구체지하철 역사의 입구를 찾아 들어간 후 전철에 탑승하여 원하는 정거장에서 하차한다. Day 4미션 1 설명1. 아래 코드와 설명을 보고, [섹션 3. 논리, 사고의 흐름]에서 이야기하는 내용을 중심으로 읽기 좋은 코드로 리팩토링해 봅시다.public boolean validateOrder(Order order) { if (order.getItems().size() == 0) { log.info("주문 항목이 없습니다."); return false; } else { if (order.getTotalPrice() > 0) { if (!order.hasCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; } else { return true; } } else if (!(order.getTotalPrice() > 0)) { log.info("올바르지 않은 총 가격입니다."); return false; } } return true; }나의 답public static final String NO_ORDER_ITEM = "주문 항목이 없습니다."; public static final String INVALID_TOTAL_PRICE = "올바르지 않은 총 가격입니다."; public static final String NO_USER_INFO = "사용자 정보가 없습니다."; public boolean validateOrder(Order order) throws OrderException { if (order.doesHaveItem()) { throw new OrderException(NO_ORDER_ITEM); } if (order.doesNotHaveValidTotalPrice()) { throw new OrderException(INVALID_TOTAL_PRICE); } if (order.doesNotHaveCustomerInfo()) { throw new OrderException(NO_USER_INFO); } return true; }Orderpublic abstract class Order { public abstract boolean doesHaveItem(); public abstract boolean doesNotHaveValidTotalPrice(); public abstract boolean doesNotHaveCustomerInfo(); }변경 사항if문의 조건들을 하나의 함수로 정의함으로써 조건의 의미를 명확히 했습니다.if문의 조건에 부합하지 않을 경우 바로 결과를 반환하게 했습니다.불필요한 부정 조건을 제거했습니다.별개의 예외 처리 클래스를 생성하여 예외를 명확히 했습니다.관심사를 기준으로 공백을 두었습니다.미션 2 설명SOLID에 대하여 자기만의 언어로 정리해 봅시다. SSRP (단일 책임 원칙)하나의 클래스는 하나의 책임(=관심사)을 가져야 한다.SRP를 지킴으로써 객체들을 관심사 기준으로 분리할 수 있다.높은 응집도와 낮은 결합도를 제공한다.응집도클래스나 모듈 내 요소들이 긴밀하게 연관되어있는 정도결합도한 요소가 변경되었을 때 다른 요소들이 영향을 받는 정도OOCP (개방-폐쇄 원칙)확장에는 열려 있고, 수정에는 닫혀 있어야 한다.기존 코드의 변경 없이도 시스템의 기능을 확장할 수 있어야 한다.추상화와 다형성을 활용함으로써 구현할 수 있다.LLSP (리스코프 치환 원칙)두 클래스가 상속 구조를 가질 때 부모 클래스의 인스턴스를 자식 클래스로 치환하여도 기능 상에 문제가 없어야 한다.자식 클래스는부모 클래스의 책임을 준수해야 한다.부모 클래스의 행동을 변경하지 않아야 한다.LSP를 위반할 경우 아래 문제가 발생할 수 있다.오동작예상 밖의 예외위 두 문제를 방지하기 위한 불필요한 타입 체크 동반IISP (인터페이스 분리 원칙)클라이언트는 자신이 사용하지 않은 인터페이스에 의존하면 안된다.ISP를 위반할 경우불필요한 의존성으로 인해 결합도가 높아진다.특정 기능의 변경이 여러 클래스에 영향을 미칠 수 있다.필요한 기능 단위로 인터페이스를 나눠서 사용해라.DDIP (의존성 역전 원칙)상위 수준의 모듈은 하위 수준의 모듈에 직접 의존해서는 안된다.추상화(인터페이스, 추상 클래스)에 의존해야 한다.의존성의 순방향고수준 모듈이 저수준 모듈을 직접 참조의존성의 역방향고수준, 저수준 모듈 모두 추상화를 참조DIP를 지킬 경우 저수준 모듈의 변경이 고수준 모듈에 영향을 미치지 않게 된다. 

[인프런 워밍업 클럽] 스터디 3기_백엔드 클린 코드, 테스트 코드 1주차 발자국

회고록 섹션 4. 객체 지향 패러다임을 들으며 미션을 통해 아직 많이 미숙하지만 SOLID에 대해 좀 더 가까워지는 계기가 되었다. public class Mission { public boolean validateOrder(Order order) { if(order.getItems().size() == 0) { log.info("주문 항목이 없습니다."); return false; }else { if(order.getTotalPrice() > 0) { if(!order.hasCustomerInfo()) { log.info("사용자 정보가 없습니다."); return false; }else { return true; } }else if(!(order.getTotalPrice() > 0)) { log.info("올바르지 않은 총 가격입니다."); return false; } } return true; } }public class MyExplanation { public boolean validateOrder(Order order) { if(order.getItems().size() == 0) { log.info("주문 항목이 없습니다."); return false; }else { return validateOrderDetails(order); } return true; } private static boolean validateOrderDetails(Order order) { if(hasNonZeroTotalPrice(order)) { return isCustomerInfoPresent (order); } log.info("올바르지 않은 총 가격입니다."); return false; } private static boolean isCustomerInfoPresent (Order order) { if(order.hasCustomerInfo()) { return true; } log.info("사용자 정보가 없습니다."); return false; } private static boolean hasNonZeroTotalPrice(Order order) { return order.getTotalPrice() > 0; } }이런식으로 풀어봤다. 현재는 여기까지가 최대이지만 나중에 강의를 다 듣고 한번 더 리펙토링 해보며 이전 코드와 비교해보는 시간을 가져볼것이다.[인프런 워밍업 클럽 스터디 3기_백엔드 클린 코드, 테스트 코드] 

서나무

[회고] PM 인프런 워밍업 클럽 3기 1주차

학습 내용 요약PM의 선호 역량은 기업마다 다르지만, 핵심은 제품으로써 고객 가치와 사업적 가치를 만들어내는 것고객 중심의 고객이 만족하는 제품을 만들어야 함PM은 제품 성과 전반을 책임지는 사람상황과 맥락에 맞는 방법론을 적용할 수 있어야 함 PMF(Product Market Fit): 시장을 만족시키는 제품을 찾기 > Feature Work(새로운 가치 창출) / Growth Work(더 많은 사용자 만들기)PM은 문제를 잘 정의해야 하고, 그 문제가 정말 중요한 것인지 판단해야 함데이터는 문제 파악에 기여, 문제 해결은 창조의 영역이며 좋은 아이디어가 필요 학습 회고사실 나는 챌린지에 참여하기 전에 미리 학습을 했는데, 강의 내용이 너무 알차고 좋아서 멈출 수 없었다...사용자의 니즈를 파악하는 방법에 대한 막연함이 있었는데 이 강의를 통해 막혀있던 것이 뚫린 느낌?지금 진행중인 사이드 프로젝트에 적용할 수 있는 것들이 많을 것 같다. 다만 분석툴을 익혀야 한다는 장벽이 있지만 그래도 해본 것과 시도조차 안한 것의 차이는 크다고 생각해서 한번 배워보려고 한다.노트에 필기를 했었는데 이렇게 회고 글로 정리해서 작성하니 더 학습한 내용을 상기할 수 있어서 좋다.미션 회고PM의 역할에 대한 아티클을 읽고 내 생각을 정리하는 미션이었는데, 처음엔 구글에 검색에서 마구잡이로 잡히는대로 읽다가 영양가 있는 글이 적다고 느껴져서 요즘IT에서 검색해서 읽었다.요즘IT는 정성이 담긴 글들이 많아서 좋은 아티클들이 많다고 생각했고, 그래서 그런지 강의에서 배운 내용들과 일치하는 내용이 많았다.

기획 · PM· PO

정예은

[워밍업클럽3기] 클린코드-박우빈 발자국 1주차

학습내용섹션1~4📝미래의 나를 위해, 미래의 자손을 위해이름 짓기는 깔쌈하게 ! 중요키워드만 뽑아서 !중요한 정보만 남기는 추상화 잘 하기 !메서드 생성클린코드를 위해 각 로직별로 추상화를 하여 메서드로 만들어주자!✅“한가지 역할” 을 하는 코드 블럭을 찾고, 메서드로 분리✅그에 맞는 메서드 “이름 “ 지어주기✅<aside> 💡⭐메서드 생성 단축키 =ctrl+alt+m</aside>   학습정리 링크https://www.notion.so/DAY02-1ab010f075ca81ed8b20fd23dead0c76?pvs=4https://www.notion.so/DAY-04-SOLID-1-1ab010f075ca81abbcd8c909d84e74ce?pvs=4👣회고👣이번 주는 SOLID 원칙을 중심으로 코드 리팩토링을 진행하며, 보다 견고하고 유지보수하기 쉬운 구조를 고민하는 시간을 보냈다. 💡 잘한 점✅ 메서드 추출을 통해 가독성을 높이고 코드의 역할을 명확히 함✅ 기존 코드를 무조건 변경하기보다는, 확장 가능성을 고려하면서 구조를 잡아나감✅ 인터페이스와 추상 클래스의 활용을 고민하며 유연한 설계를 연습함 ⚠ 아쉬운 점아직은 강사님이 따라하는 대로 코드를 있는 그대로 따라치기만 하는 과정으로 수업을 들었음하나하나씩 로직과 메서드들을 분석해가며 수업을 들으려니, 30분 수업은 나에게 60분이되어 돌아왔음그만큼 시간을 오래 잡아먹기 때문에 진도 맞추기가 너무 어려웠다..내가 이 로드맵을 참여한게 올바른 선택이긴 할까? 라는 고민도 많이 들었지만, 일단 코드 100번정도 따라쳐보면 대충 흐름이 파악되지 않을까? 생각하며 수업을 듣고 노션에 정리하던 한주였다....  🎯 다음 주 목표단순히 원칙을 따르는 것이 아니라, 상황에 맞는 적용법을 체득하기미션을 해결할 때, "왜 이렇게 설계했는가"를 먼저 고민하고 코드를 작성하는 습관 들이기   📢미션📢Day02추상 : 눈사람을 만든다 구체 :대기중에 떠다니는 먼지가 핵이 되어, 이 핵을 중심으로 수증기가 응결해가며 형성되는 결정체의 집합체를 손으로 뭉친다2덩이로 둥글게 뭉쳐서 몸통과 머리로 붙여준다주변에 굴러다니는 , 자연에서 산출되는, 생물이 아닌 단단한 고체 물질을 눈과 코에 붙여준다  Day04SOLID원칙단일책임원칙클래스는 하나의 책임만 가져야 한다.책임을 인지하고 분리하고 다른 클래스 만들기.메인 도입부에 게임 실행부 넣지 않고 → 지뢰찾는 로직을 담은 클래스를 하나 생성해서 하나의 책임만 갖도록 Minesweeper 개방 폐쇄 원칙기존 코드를 많이 변경하지 않고 확장할 수 있도록 설계하기 추후 유지보수나 조건들이 추가로 생겨날때 당황하지 않도록 너무 상수로만 값이나 데이터 정의 내리지 않기리스코브 치환 원칙자식은 부모를 대체해서 일할 수 있고, 부모는 자식을 대체할 수 없다. 부모 클래스를 사용하는 곳에 자식 클래스를 넣어도 문제가 없어야 함인터페이스 분리 원칙하나의 커다란 인터페이스 사용하는게 아니라, 여러개의 인터페이스로 분리하기 하나의 인터페이스에는 하나의 메서드만 , 관련된 메서드만 넣어야함의존성 역전 원칙구체적인 구현 클래스가 아니라, 인터페이스나 추상 클래스에 의존 하도록 설계  public boolean validateOrder(Order order) { if (isInvalidOrder(order)) { return false; } return true; } private boolean isInvalidOrder(Order order) { if (order.doesNotHaveAnyItem()) { log.info("주문 항목이 없습니다."); return true; } if (order.doesNotHaveCustomerInfo()) { log.info("사용자 정보가 없습니다."); return true; } if (order.hasNegativeTotalPrice()) { log.info("올바르지 않은 총 가격입니다."); return true; } return false; } 👣회고👣미션을 해결하면서 "추상화"의 중요성을 몸소 체감한 한 주였음특히, 눈사람 만들기 예제를 통해 구체적인 행동을 추상화하는 연습을 했고, 이를 코드에도 적용하려 노력했다. 

백엔드워밍업클럽워밍업클럽3기박우빈백엔드백엔드스터디지뢰찾기클린코드리팩토링

[인프런 워밍업 클럽 3기 - BE 클린 코드, 테스트 코드] 1주차 발자국 👣

1주차 발자국 👣 💻 강의 수강👩🏻‍💻 학습 내용 요약잘 읽히는, 이해할 수 있는 코드를 작성하자.추상화는 구체적인 정보들로터 중요한 정보들만 남기는 것이다. 추상화에서 도메인의 문맥이 중요하다.이름을 짓는 행위도 추상화의 일종으로, 단어만으로 읽는 이에게 정보를 쉽게 전달할 수 있어야 한다.메서드의 주제는 반드시 하나여야 한다. 포괄적인 의미를 담아야 하며, 작은 단위의 메서드로 쪼갤 수 있다.상수와 줄 바꿈으로도 가독성을 높일 수 있다.코드를 읽는 이가 너무 많이 고민하지 않게, 기억해야 하는 정보가 많지 않게 읽을 수 있게 하자.Early return, 사고 과정의 depth 줄이기, 부정어 대체하기 등예외가 발생할 가능성을 낮추자. 의도한 예외와 예상하지 못한 예외를 구분하자.Java에서 null로 인한 예외를 주의하자. Optional 사용 고민해 보자.  🤔 학습 내용에 대한 회고강의를 듣고 이해하며 미션을 수행하는데 시간이 빠듯했다.하나하나 기록하며 강의를 듣는 편인데 이러한 방식은 시간이 오래 걸린다. 어떻게 해야 적은 시간 내에 강의 수강 및 이해, 미션 수행이 가능할 지 학습 방법에 대해서 고민해봐야겠다.강의가 너무 재밌다!!!💙 🎯 다음 주 학습 목표효율적인 학습 방법을 알아내서 적용해보자!!  ✉ 미션💭 미션 해결 과정Day 2 미션 우리 가족 귀염둥이와 산책하는 것에 대해서 구체화해보았다. Day 4 미션1번 내용)강의에서 언급한 논리, 사고의 흐름에 따라서 읽기 좋은 코드가 되도록 리팩토링하였다.2번 내용)SOLID 원칙에 대해서 나만의 언어로 작성하였다. 🤔 미션 해결에 대한 간단한 회고미션을 잘 한 건지 모르겠다.. 피드백을 받고 싶다...  📚 출처[강의] Readable Code: 읽기 좋은 코드를 작성하는 사고법 https://www.inflearn.com/course/readable-code-%EC%9D%BD%EA%B8%B0%EC%A2%8B%EC%9D%80%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%EC%82%AC%EA%B3%A0%EB%B2%95/dashboard 

백엔드워밍업클럽백엔드clean-code객체지향리팩토링

kailis

발자국 1주차: 읽기 좋은 코드와 현실 비즈니스 속 객체지향의 관계

 해당 글은 인프런 워밍업 클럽 스터디 3기 - 백엔드 클린 코드, 테스트 코드를 진행하며 깊게 고민해 보려고 한 내용을 담습니다. 단순한 이해보다는 현실적 적용에 집중합니다.   우리는 자바를 기반한, 객체지향이라는 패러다임을 담은 개발을 하며 <객체지향>의 중요성을 귀가 닳도록 듣는다. 책임의 분리. 어디까지가 객체의 책임인가? 어떤 것은 추상이고 어떤 것이 구상인가? 책임이라는 개념은 객체지향에서 매우 중요하지만, 그와 동시에 현실에서 적용하는 데에서는 생각할 부분이 많다. 나는 비즈니스 일정과 아름다운 코드, 그리고 복잡성에 대한 사이에서 그동안 고민을 많이 해 왔었다. 강의를 들으면서 그것들을 고민하고자 했고, 그 고민에 대한 이번 주 일주일 동안의 이야기를 해 보려고 한다.  Graceful Code와 Business 사이에서 사실 SOLID? 안다. 내 연차에서 그것을 모르는 자바 개발자는 드물 것이다. 하지만 그 개발자들은 나와 같이 이 SOLID를 어떻게 적용할지에 대해 고민할 것이다. 따라서 나는 이번 주에는 SOLID 원칙을 실무적으로 적용하는 과정에서 현실적인 한계를 분석하는 데 집중하려 했다. 특히, 기존 시스템에 SOLID 원칙을 도입하려 할 때 발생하는 문제점을 파악하고, 이를 해결하기 위한 현실적인 방법을 고민하고자 했다. 그래, 현재 코드베이스의 특성을 고려하여 현실적인 타협점을 찾는 방법을 고민해 보고 싶었다. 강의를 보며 생각해 본 이번 주의 고민들을 녹이며, 지금부터 SOLID 원칙을 한 가지씩 짚어가면서 이야기해 보고자 한다. 원칙과 현실의 부딪힘 SRP(단일 책임 원칙) 적용의 현실적 고민 사실 제일 좋아하는 원칙이다. SOLID의 가장 핵심이라고 생각하기도 한다. 그렇지만 책임이란 단어는 참 모호하다. [카페]에서 [커피]를 [주문]한다고 했을 때, [주문]이 담당하고 있는 책임은 어디까지일까? 이와 같이 나는 SRP를 고민할 때, [어디까지가 객체의 책임]인가에 대해서 고민한다. class CafeOrderService { private PaymentProcessor paymentProcessor; private NotificationService notificationService; private TransportService transportService; public void processOrder(Order order) { paymentProcessor.process(order); notificationService.sendOrderConfirmation(order); transportService.giveDrink(); } } 카페 주문은 [결제] / [완료 안내] / [손님에게 음료 주기]를 포함할 수 있다. 그런데 카페의 할 일이 많아지게 되면, 이 클래스는 너무 많은 책임을 가지게 된다. 만약 카페가 배달 서비스를 시작하면 카페의 책임은 [배달] / [배달 기사님 콜 부르기] / [배달 완료표시] 등으로 확장될 수 있다. 카페의 할 일은 많지만 책임의 갯수가 늘어날 때는 역시 분리를 고민해야 할 것 같다. 그럼 그걸 몇 개로 제한해야 할까? 3개? 4개? … 개수가 아니지 않을까? 내가 이번에 정의를 내리게 된 것은 SRP가 단순하게 ”하나의 클래스 = 하나의 책임“이 아니라. “변경의 이유가 하나인가”를 고민하는 원칙이라는 것이다. 따라서 우리는 카페 주문이라는 서비스가 “언제” 바뀔지에 따라 응집도의 기준을 정해야 한다. 가령 [카페 주문]이라는 것이 여러 방식의 주문을 가질 수 있게 된다면(방식의 개수가 바뀜), [매장 카페 주문] / [배달 카페 주문]으로 바꾸고, 외부 인터페이스는 타입에 따라 분리하도록 지정하는 것이다. // 주문 처리를 위한 상위 인터페이스 interface OrderService { void processOrder(Order order); } // 매장 주문 처리 class StoreOrderService implements OrderService { private PaymentProcessor paymentProcessor; private NotificationService notificationService; private ServeService serveService; @Override public void processOrder(Order order) { paymentProcessor.process(order); notificationService.sendInStoreOrderConfirmation(order); serveService.serveDrinkAtCounter(order); } } // 배달 주문 처리 class DeliveryOrderService implements OrderService { private PaymentProcessor paymentProcessor; private NotificationService notificationService; private DeliveryService deliveryService; private DriverNotificationService driverService; @Override public void processOrder(Order order) { paymentProcessor.process(order); notificationService.sendDeliveryOrderConfirmation(order); deliveryService.prepareForDelivery(order); driverService.notifyAvailableDriver(order); } } // 주문 타입에 따라 적절한 서비스를 선택하는 팩토리 class OrderServiceFactory { private StoreOrderService storeOrderService; private DeliveryOrderService deliveryOrderService; public OrderService getOrderService(OrderType orderType) { switch (orderType) { case STORE: return storeOrderService; case DELIVERY: return deliveryOrderService; default: throw new UnsupportedOperationException("지원하지 않는 타입:" + orderType); } } } // 클라이언트 코드 class CafeOrderController { private OrderServiceFactory orderServiceFactory; public void processOrder(Order order) { OrderService orderService = orderServiceFactory.getOrderService(order.getType()); orderService.processOrder(order); } }  이제 OrderService 입장에서는 책임이 분리됐다. 타입만 ENUM에서 선택해서 넘겨 주면 되겠다. 그런데 여기서 튀어나오는 원칙이 하나 더 있다. OCP다. OCP(개방-폐쇄 원칙) 적용의 현실적 고민 OCP. 인터페이스 정의의 핵심이다. 특히 전략 패턴에서 고민하게 되는 부분인 것 같다. OCP 이야기가 많이 나오는 예제로 Oauth 로그인이 있다. 네이버 / 카카오 인증을 인터페이스로, 행위 중심을 토대로 폐쇄하되 앞으로 여러 가지 인증의 가능성을 넓히는 것. 그런데 나는 확장의 필요성과 지속성을 먼저 고려해야 한다고 생각한다. “우리는 이 객체를 어디까지 확장할 것인가?” 확장성을 높이기 위해 인터페이스를 도입했지만, 너무 많은 추상화가 오히려 코드 가독성을 해치는 경우도 있을 수 있다. 이번에 크리스마스 특집으로 행사를 한다고 한다. 이 행사는 일주일간 일어나고 사라질 것이다. 그럼 우리는 여러 가지 이벤트가 발생할 때마다 늘 DiscountPolicy의 구현체를 추가해 주어야 하는 것일까? 위의 예시에서도, 이벤트성으로 음료 주문 방식에 증정 이벤트가 추가되었다고 해 보자. 그렇다면 증정 이벤트가 추가된 클래스를 만들어야 할까? 뭐, 그럴 수도 있다. 클래스는 쓰다 지우면 된다. 그런데 추가된 클래스를 나중에 삭제할 수 있다고 쉽게 생각하지만, 실제로는 코드베이스에 남아 오히려 누군가 옵션을 더하는 식으로 클래스가 커져, 유지보수 비용이 발생할 수 있다. 같은 소스코드 안에 있다면 3줄로 관리하면 되는데, 클래스를 하나 늘리는 것이 추후 휴먼 오류 가능성을 높이는 행동이 될 수 있다는 것이다. (세상에는 객체지향을 사랑하는 사람만 있지는 않다) 결국 객체의 [개방]을 위해 보장해야 하는 것은 로직이 얼마나 지속될지의 여부인 것 같다. 그렇다면 리스코프 치환 원칙은 어떠한가? LSP(리스코프 치환 원칙) 적용의 현실적 고민 리스코프 치환 원칙의 중심은 부모에게 있다. 부모가 하는 일을 자식이 위반하지 않아야 하는 것이다. 여기서 가장 적용하기 모호해지는 것은 [부모가 하는 일]이다. 부모는 어떠한 책임을 가질까? 그리고 어떠한 행위를 할까? 역할이 모호한 만큼, 부모의 역할 또한 모호하게 느껴진다. 우리의 카페 주문 예시로 생각해 보자. 사람은 카페에게 기대하는 역할이 있다. 나에게 내가 원하는 음료수를 주는 것이다. 그것이 배달이든, 실제로 가서 주문하는 것이든 달라지는 것은 없다. 여기서 가장 중요한 것은 나 / 음료수 / 전달 이다. 나는 리스코프 원칙을 [클라이언트가 기대하는 응답을 주는 것]을 부모가 하는 일을 위반하지 않는 것이라고 생각한다. 우리의 카페 주문에서의 리스코프 책임 원칙은, “부모가 가진 인터페이스의 계약을 지키는 것”은, [음료수를 전달하는 것]일 것이다. 방식은 다르더라도 음료수만 잘 배달하면 된다. 그러면 지킨 것이다. 그렇다면 스프링에서 가장 많이 쓰이는 DIP는 어떨까? DIP(의존성 역전 원칙) 적용의 현실적 고민 DIP는 고수준 모듈이 저수준 모듈에 의존하지 않고 둘 다 추상화에 의존하게 만든다. 스프링 프레임워크에서는 이 원칙을 기반으로 DI(의존성 주입)를 제공한다. 개발자로서 우리는 스프링에게 객체 생성을 위임하고 수많은 DI를 수행한다. new를 직접 호출할 필요가 없다니! 정말 편리하다. 그런데 모든 의존성을 인터페이스로 추상화하는 것이 항상 최선일까? 다음과 같은 상황을 고려해보자. interface UserService { User findById(Long id); void register(User user); } class CafeUserService implements UserService { // 카페 유저 관련 구현 } class StoreUserService implements UserService { // 상점 유저 관련 구현 } 현재 서비스에는 카페 사용자만 존재하고, 상점 사용자는 아직 구현 계획이 없다. 그럼에도 불구하고 “미래의 확장성”을 위해 인터페이스를 도입해야 할까? 인터페이스를 도입하면 분명 유연성을 얻을 수 있지만, 당장 StoreUserService 구현체가 필요하지 않다면 이는 불필요한 복잡성을 가져올 수 있다. 게다가 초기 스타트업에서 이 부분은 두드러진다. 도입 가능성을 예측했으나 갈아엎어지는 기획이 너무나 많으니까... 따라서 DIP 적용의 균형점은 근미래의 변경 가능성 / 팀의 개발 문화에 있다고 생각한다. 물론 대규모 엔터프라이즈 애플리케이션에서는 철저한 DIP 적용이 장기적으로 유리할 수 있다. 작은 프로젝트나 스타트업에서는 과도한 추상화가 오히려 개발 속도를 늦출 수 있다. “도입이 상상되는 인터페이스”는 애자일과 맞지 않는다. 결국 DIP의 적용은 실용적 균형의 문제가 아닐까 생각해 본다. 이제 마지막, ISP에 대해서 생각해 보았다. ISP(인터페이스 분리 원칙) 적용의 현실적 고민 ISP의 케이스에서 가장 경계해야 할 것은 결국 [개수]라는 생각을 한다. 얼마나 분리할 것인가? 얼마나 분리하는 것이 효율적인가? 나는 [현재 상태에서 필요한 만큼]이라고, 그러니까 최소한이라라고 생각한다. 많은 인터페이스가 생겼을 때 가장 큰 문제는 아무래도 파일이 최소 2배가 된다는 점이다. 수많은 파일은 복잡성을 높이는 것이 사실이니까. 어떠한 아키텍처가 이 수많은 인터페이스들을 깔끔하게 감당할 수 있는가? Spring과 같은 DI 프레임워크에서는 이런 문제가 더욱 두드러질 수 있다. 수많은 작은 인터페이스들의 구현체를 모두 Bean으로 등록하고 관리해야 하기 때문이다. 동시에 나는 섣부른 인터페이스 분리를 가장 경계해야 한다고 생각한다. 인터페이스가 재정의되어야 하는 순간, 기존에 해당 인터페이스들을 사용하고 있는 객체의 로직을 전부 다 다시 살펴야 한다. 따라서 ISP 또한... 먼저 상상하지 않는 것이 중요해 보인다. 이 인터페이스를 구현한 뒤 시일이 지난 이후, 다른 구현체에서도 상태가 변하지 않을 인터페이스만을 최대한 고민해 보려 한다. 잠시 응집도가 떨어지더라도.  회고 우리는 현실과 아름다운 코드의 사이에서 무엇을 포기해야 하는가? 객체의 책임은 비즈니스의 상황마다 가변적일 수 있다는 생각을 하기 때문이다. 인터페이스가 많으면 아름답다. 하지만 한눈에 파악하는 것은 어려워진다. 전략패턴은 객체의 책임을 확장성있게 분리한다. 하지만 어떠한 객체를 할당할지 정하는 구체적인 룰이 정해져야 한다. 결국 아름다움은 집단의 룰을 포함한다. 혼자서는 무한대로 아름다울 수 있지만, 같이 하는 현실에서도 아름다움을 추구하는 것이 Readable한 코드의 핵심이라고 생각한다. 비즈니스적으로 빠른 코드 ≠ 읽기 좋은 코드 ≠ 아름다운 코드 장기적인 관점에서 아름다운 코드는 결국 집단이 얼마나 동일한 룰을 체득하고 있는지에 따라 달라진다고 생각한다. 그리고 우빈님의 강의는 그 룰에 대한 가이드라인을 주고 있다는 생각이 들었다. 이 룰을 체득한다면, 적어도 비즈니스를 위한 코드를 생각할 때 객체지향의 관점을 망치지는 않을 것이다. 다음 주 계획 다음 주에는 내가 알고 있는 클린코드와, 강의에서 이야기하는 클린 코드를 적용하면서 TDD를 공부해 볼 예정이다.프로젝트 한 가지를 가지고 와서 이야기를 하면 좋을 것 같아서, Gilded Rose를 가지고 와 봤다.유명한 리팩토링 kata 라이브러리니 프로젝트를 [적용]하는 방식에 있어서 많은 것을 이야기해 볼 수 있을 것 같다.테스트 코드에 대해서도 강의를 베이스로 해서 Junit을 연습해 보고자 한다.   

[인프런 워밍업 스터디] 추상과 객체 지향의 구체화

인프런 ‘Readable Code: 읽기 좋은 코드를 작성하는 사고법’을 수강한 후, 작성한 내용입니다.📌 1주차 기간 강의섹션 2. 추상코드를 잘 짠다는 것은?개발자라면 코드를 잘 짜기 위해 노력한다. ‘코드를 잘 짠다’라는 것은 무엇일까?코드를 잘 짠다는 것은 이해하기 쉬운 코드, 즉 ‘읽기 좋은 코드’라는 것에 동의하지 않는 사람은 없을 것이다.내가 짠 코드를 읽는 대상은 결국 나와 동료이다. 미래의 나와 동료를 위해 매 순간 읽기 좋은 코드를 작성하려고 노력해보자.클린 코드와 리팩토링의 가장 좋은 예시는 테스트 코드 생성 사이클로 볼 수 있다.리팩토링 대상/범위 확인기능 보장을 위한 테스트 코드리팩토링 & 테스트 코드로 검증클린 코드를 추구하는 이유그런데 클린 코드를 추구하는 이유는 뭘까?코드가 잘 읽힌다 ⇒ 이해가 잘 된다 ⇒ 유지보수하기 수월하다 ⇒ 시간과 자원이 절약된다!즉, 클린 코드는 우리의 시간과 자원을 절약해준다.클린 코드, 그리고 추상과 구체추상은 클린 코드를 관통하는 주제이다. 그렇다면 추상이란 뭘까?추상 : 중요한 정보는 가려내어 남기고, 덜 중요한 정보는 생략하여 버린다.추상의 반대편은 구체라고 볼 수 있다. 하나의 예시를 들어보자.질문A랑 주말에 뭐 했어?답변답변 1 : 식당에 예약해서 갔다왔어.답변 2 : 케치테이블을 통해 가고자 하는 식당에 들어가서 가고자하는 날짜와 시간을 선택하고 그때 식당에 들어가서 밥을 먹고왔어.답변 1과 답변 2는 같은 의미를 나타낸다. 그런데 표현은 다르다. 답변 2는 구체적인 사실이고 답변 1은 추상화된 문장이라고 할 수 있다.구체에서 추상으로 갈수록 정보는 함축되어 제거되고, 추상에서 구체로 갈수록 생략된 정보를 유추하고 재현한다.이를 개발자답게 컴퓨터 과학에 적용해보자.int는 4 byte이고, char는 1 byte이다. 같은 byte로 이루어지지만, 데이터를 어떻게 읽는지에 따라 달라진다. 즉, 데이터 개념에도 추상화가 존재하는 것이다!잠깐 프로그램의 정의에 대해 생각해보자. 프로그램은 다음과 같이 말할 수 있다.프로그램 = 데이터 + 코드위의 예시를 통해 데이터 개념에 추상화가 존재함을 알 수 있었다. 당연히 코드에도 추상화가 존재한다. 또한 데이터 + 코드에도 추상화가 존재한다.흔히 고수준/저수준 언어 라는 용어를 들어봤을 것이다. 고수준과 저수준이 나뉘는 이유는 추상화에 대한 수준을 나타내기 때문이다. 잊고 있을 수 있지만, 컴퓨터는 0과 1밖에 모른다!적절한 추상화는 복잡한 데이터와 복잡한 로직을 단순화하여 이해하기 쉽게 한다!만약 어느 도시에서 예약을 선택이라고 한다고 가정해보자. 그렇다면 답변 1은 다음과 같이 바뀔 것이다.식당을 선택해서 갔다왔어.이 말을 들으면 무슨 말인가 싶다. 말을 통해 유추하거나 재현하기 쉽지 않다. 단순히 말이 안된다고 생각할 수 있지만 이유에 대해 따져보자.추상화 과정에서 중요한 정보를 남기지 않았다.식당을 방문 날짜를 미리 정하는 예약이 아니라, 단순히 선택했다는 정보만 남김상대적으로 덜 중요한 정보를 남기고 중요한 정보를 삭제해석자가 동일하게 공유하는 Context가 없다.중요한 정보의 기준이나 도메인 영역 별 추상화 기준은 다를 수 있음해당 도시에 살았으면 이해 가능잘못된 추상화가 야기하는 사이드 이펙트는 상당히 크다!적절한 추상화란 해당 도메인의 문맥 안에서 정말 중요한 핵심 개념만 남겨서 표현하는 것이다.이름 짓기이름을 짓는다는 행위는, 추상적 사고를 기반으로 한다.단수와 복수 구분하기이름 줄이지 않기은어/방언 사용하지 않기좋은 코드를 보고 습득하기좋은 이름을 짓기 위해 노력하자!메서드 추상화메서드 이름으로 구체적인 내용을 추상화할 수 있어야 한다.서점에서 책을 샀다.서점에 가서 책을 고른다. 책을 계산대에 가져가 직원에게 건네주고 책 가격 금액을 전달하고 책을 얻었다.잘 쓰여진 코드라면, 한 메서드의 주제는 반드시 하나다!그런데 내용이 다음과 같다고 해보자.서점에서 책을 샀다.돈을 인출하고 가는길에 아이스크림을 사먹고, 책을 구매했다.추상화된 내용을 보고 구체적인 내용의 유추가 어렵다. 의미를 담을 수 있는 더 작은 단위로 쪼개야 한다.현금 인출아이스크림 사먹기서점에서 책 구입하기메서드 작성메서드 선언부반환타입메서드명파라미터메서드명추상화된 구체를 유추할 수 있는, 적절한 의미가 담긴 이름파라미터와 연결지어 더 풍부한 의미를 전달할 수도 있다.파라미터파라미터의 타입, 개수, 순서를 통해 의미를 전달파라미터는 외부 세계와 소통하는 창어떤 재료가 필요한지 알려주는 역할이다. 외부 세계한테 필요한 재료를 요구한다.반환타입메서드 시그니처에 납득이 가는, 적절한 타입의 반환값 돌려주기반환 타입이 boolean인데, 이게 이 메서드에서 무엇을 의미하는거지?void 대신 충분히 반환할 만한 값이 있는지 고민해보기반환값이 있다면 테스트도 용이해진다.코드의 줄 수가 많아서 추상화하는 것이 아니다! 같은 라인 수를 가지더라도 추상화할 수 있다.추상화 레벨메서드를 추출한다 ⇒ 외부 세계와 내부 세계를 나누고, 추상화 레벨이 나뉜다.하나의 세계 안에서는, 추상화 레벨이 동등해야 한다!게임 시작 멘트 출력게임 초기화게임 보드 보여주기게임 상태가 1이면게임 종료 메시지 출력1, 2, 3, 4를 쭉 읽다보면 4에서 멈칫하게 된다. 게임 상태 = 1이라는 것의 의미에 대해서 의문이 생기게 된다. 즉, 읽는 사람이 해석을 하게 만든다. 따라서 동등한 추상화 레벨을 맞춰야 한다.게임 시작 멘트 출력게임 초기화게임 보드 보여주기게임을 이겼다면,게임 종료 메시지 출력전보다 훨씬 읽기 수월하다.메서드로 추출한다는 것은 로직이 복잡하거나 의미를 부여할 수 있어서도 맞지만, 추상화 레벨을 동등하게 맞춤으로써 읽는 사람으로 하여금 자연스럽게 이해할 수 있게 한다.매직 넘버, 매직 스트링의미를 갖고 있으나, 상수로 추출되지 않은 숫자, 문자열 등상수 추출로 이름을 짓고 의미를 부여함으로써 가독성, 유지보수성 증가섹션 3. 논리, 사고의 흐름인지적 경제성코드를 작성할 때 인지적 경제성을 추구하도록 작성해보자!💡 인지적 경제성? - 최소의 인지적 노력으로 최대의 정보 제공 - 한 번에 한 가지 일 즉, 최소한의 인지만 가져가서 최대의 효율을 내보자라는 뜻!코드를 읽는 사람의 메모리를 효과적으로 쓸 수 있도록 해서 읽기 좋은 코드를 작성하자.Early Returnelse if ⇒ 이전의 if에 대해서 생각한다.else ⇒ 앞의 조건들을 모두 알아야 안다.즉, 앞 선 정보들을 모두 기억하고 있어야 함!Early Return을 이용하자!else가 사라짐앞의 코드를 신경 쓸 필요가 없다.사고의 depth 줄이기중첩 분기문, 중첩 반복문2중 for문 내에 if 문이 있다면?3 depth ⇒ 사고의 depth도 많을 것!메서드 추출을 통해 중첩 분기문/반복문을 없애자! 그렇다고 무조건 1 depth로 만들라는 것은 아니다.추상화를 통한 사고 과정의 depth를 줄이는 것이 중요2중 중첩 구조로 표현하는 것이 사고하는 데 더 도움이 된다면 그대로 놔두자.사용할 변수는 가깝게 선언하기사용할 변수는 가깝게 선언하기사용할 변수가 너무 멀리있다면 되돌아가도록 유도한다.공백 라인도 의미를 가진다!복잡한 로직의 의미 단위를 나누어 보여줌으로써 읽는 사람에게 추가적인 정보를 전달할 수 있다.우리나라 글도 문단이 있지 않은가!부정어를 대하는 자세!가 있으면 조건을 먼저 이해하고, 이에 반대되는 조건을 생각하게 된다.가독성이 떨어지고 비틀어서 생각하게 함읽으면서 바로 생각할 수 있게 조건을 작성하자.해피 케이스와 예외 처리예외가 발생할 가능성 낮추기검증이 필요한 부분 = 외부 세계와의 접점의도한 예외와 예상하지 못한 예외 구분사용자에게 보여줄 예외개발자가 보고 처리해야 할 예외항상 NPE가 일어나지 않는지 고려하자.메서드 설계 시 return null 자제Optional은 비싼 객체이고 반환 타입에 사용한다.orElse, orElseGet, orElseThrowe.printStackTrace는 실무에서는 안티패턴이다!섹션 4. 객체 지향 패러다임추상의 관점으로 바라보는 객체 지향절차 지향정해진 순서 차례대로 처리하는 프로그래밍객체 지향객체를 만들어서 객체들 간의 협력을 통해 이루어지는 프로그래밍함수형순수 함수를 정의Side Effect가 없는 함수A ⇒ 항상 A에 대한 정해진 결과가 나오는 것객체란?추상화된 데이터 + 코드객체간의 협력과 객체가 담당하는 책임객체 지향의 특징 ⇒ 캡슐화, 추상화, 상속, 다형성정말 이를 이해하고 적용하고 있는지 스스로 질문해보자!코드 레벨에서 잘 녹여서 쓸 수 있어야 한다.관심사의 분리유지보수가 원활해짐높은 응집도와 낮은 결합도같은 관심사는 응집도가 높아야 한다.각 관심사끼리는 결합도가 낮아야 한다.객체 설계객체로 추상화는..비공개 필드(데이터), 비공개 로직(코드)공개 메서드 선언부를 통해 외부 세계와 소통객체의 책임이 나뉨에 따라 객체 간 협력 발생!객체를 통해절차 지향에서 보이지 않았던 개념 가시화관심사가 한 군데로 모이기 때문에 유지보수성 증가객체를 사용하는 입장에서 보다 높은 추상화 레벨에서 도메인 로직을 다룰 수 있다!!주의사항1개의 관심사로 책임이 분리되었는지 스스로 질문해보자.객체 생성 시, 유효성 검증이 가능setter는 지양, 필요할 경우 메서드명에 의도를 드러내자.getter는 필요한 경우에만.필드 수는 적을 수록 좋다.불필요한 데이터를 굳이 가지고 있지 말자. ex) 주문의 총 가격도메인 지식은 만드는 것이 아니라 발견하는 것!SRP (단일 책임 원칙)하나의 클래스가 하나의 책임만 갖도록 설계해라!하나의 클래스는 한 가지의 변경 이유만 가진다.객체가 가진 공개 메서드, 필드, 상수 등은 해당 객체의 단일 책임에 의해서만 변경 되는지 질문하자.변경이유 = 책임관심사의 분리높은 응집도, 낮은 결합도응집도 = 클래스나 모듈 내 요소들이 서로 긴밀하게 연관되어 있는 정도결합도 = 두 개 이상의 객체가 협력할 때, 한 객체가 변경되었을 때 다른 객체가 영향받는 정도각 객체가 독립적인 책임을 가지도록 잘 쪼개진다.객체를 하나의 변경 지점, 하나의 책임으로만 가지도록 설계하자!책임을 발견하기 어려움경계선이 사람마다, 도메인마다 다를 수 있음설계한 객체가 하나의 책임만 가지고 있는지 끊임없이 질문책임을 보는 눈을 기르자!객체를 설계했을 때 책임이 잘 응집되었는가?, 단일 책임만을 가지고 있는가?OCP (개방-폐쇄 원칙)확장에는 열려 있고, 수정에는 닫혀 있어야 한다.기존 코드 변경 없이 기능 확장이 가능해야 함!추상화와 다형성을 활용하여 OCP를 지키자.새로운 요구사항이 추가되었을 때, 기존 코드가 과도하게 변화된다?OCP를 지키지 못하고 있는게 아닌지 생각하자.LSP (리스코프 치환 원칙)상속 구조에서, 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 치환할 수 있어야 한다.자식 클래스는 부모 클래스의 책임을 준수하고 행동을 변경하지 않아야 한다.부모가 일하는 곳에 자식이 가더라도 문제가 없어야 한다!LSP 위반상속 클래스를 사용할 때 오동작 발생ISP (인터페이스 분리 원칙)클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안 된다.기능 단위로 인터페이스를 잘게 쪼개라!내가 구현하려는 인터페이스의 기능 명세에 사용하지 않는 기능이 있다.자신이 사용하지 않는 인터페이스에 의존하게 됨불필요한 의존성으로 인해 결합도가 높아진다.특정 기능의 변경이 여러 클래스에 영향을 미친다.DIP (의존성 역전 원칙)상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다.둘 모두 추상화에 의존해야 한다.의존성의 순방향 : 고수준 모듈이 저수준 모듈을 참조하는 것의존성의 역방향 : 고수준, 저수준 모듈이 모두 추상화에 의존하는 것저수준 모듈이 변경되어도, 고수준 모듈에는 영향이 가지 않는다.기능을 추상화하여 고수준 모듈은 추상화된 스펙만 참조추상화를 중간에 두고 의존하도록 하자!저수준 모듈이 자유롭게 변경되도 고수준 모듈에는 영향 X💡스프링에서의 DI, IoC DI = Dependency Injection - 필요한 의존성을 직접 생성하는 것이 아니라, 외부에서 주입받는다. - 제 3자(스프링 컨텍스트)가 런타임 시점에 의존성을 주입해준다. IoC = Inversion of Control - 프로그램의 흐름을 프레임워크가 담당 - 빈 생성, 의존성 주입, 생명주기 관리를 스프링 컨테이너가 해준다. 섹션 5. 객체 지향 적용하기상속과 조합상속보다 조합을 사용하자.상속은 시멘트처럼 굳어지는 구조수정이 어렵다.부모와 자식의 결합도가 높다.자식이 부모에 대해서 다 알고 있어야 함부모 수정 ⇒ 자식이 다 영향을 받는다.자식은 부모에 있는 필드를 직접적으로 알고 있음조합과 인터페이스를 활용하는 것이 유연한 구조상속을 통한 코드의 중복 제거가 주는 이점보다, 중복이 생기더라도 유연한 구조 설계가 주는 이점이 더 크다.Value Object도메인의 어떤 개념을 추상화하여 표현한 값 객체값으로 취급하기 위해 불변성 동등성 유효성 검증 등을 보장해야 한다.불변성 : final 필드, setter 금지동등성 : 다른 인스턴스여도 내부 값이 같으면 같은 값 객체로 취급유효성 검증 : 객체가 생성되는 시점에 값에 대한 유효성 보장VO vs EntityEntity식별자 O식별자가 아닌 필드의 값이 달라도, 식별자가 같으면 동등한 객체로 취급!equals & hashcode도 식별자 필드를 통해 재정의VO식별자 X내부의 모든 값이 다 같아야 동등한 객체일급 컬렉션일급 시민다른 요소에게 사용 가능한 모든 연산을 지원하는 요소변수로 할당될 수 있다.파라미터로 전달될 수 있다.함수의 결과로 반환될 수 있다.일급 함수함수형 프로그래밍에서 함수는 일급 시민함수는 변수에 할당될 수 있고, 인자로 전달될 수 있고, 함수의 결과로 함수가 반환될 수도 있다.일급 컬렉션컬렉션을 포장하면서, 컬렉션만을 유일하게 필드로 가지는 객체컬렉션을 다른 객체와 동등한 레벨로 다루기 위함단 하나의 컬렉션 필드만을 가진다.컬렉션을 추상화하며 의미를 담고, 가공 로직의 보금자리가 생긴다.가공 로직에 대한 테스트도 작성 가능getter로 컬렉션을 반환할 일이 생긴다면, 외부 조작을 피하기 위해 새로운 컬렉션을 만들어 반환해야 한다.Enum의 특성과 활용enum은 상수의 집합상수와 관련된 로직을 담을 수 있음특정 도메인 개념에 대해 종류와 기능 명시변경이 잦은 개념은 DB로 하는게 나을 수도 있다.enum은 코드 ⇒ 배포를 해야만 변경할 수 있다.변경이 잦은 요소라면 DB로 관리하자.숨겨져 있는 도메인 개념 도출하기도메인 지식은 만드는 것이 아니라 발견하는 것객체 지향은 현실을 100% 반영하는 도구가 아니라, 흉내내는 것현실 세계에서 쉽게 인지하지 못하는 개념도 도출해서 사용해야 할 때가 있다.설계할 때는 근시적, 거시적 관점에서 최대한 미래를 예측하고, 시간이 지나 틀렸다는 것을 인지하면 언제든 ㄷ돌아올 수 있도록 코드를 만들어야 한다.완벽한 설계 X, 그 당시의 최선이 있을 뿐 📌 1주차 수강 후 느낀점강의 제목들을 보면, 사실 개발자라면 다 한번씩 들어봤을 내용이다. SOLID 원칙, 추상화, VO, Enum 등등 누군가 질문하면 그럴듯한 답변을 할 수 있는 주제다. 그런데 ‘이 내용들을 깊이 있게 이해하고 있을까?’ 라는 질문에는 아니었던 것 같다.이러한 개념들을 직접 코드 레벨에 적용하며 개선하는 과정에서 배울 점이 많았다. 내가 알고 있던 깊이는 상당히 얄팍했다. 가끔 헷갈리면 검색해서 ‘아 대충 이런내용이었지.’하고 금방 닫곤 했는데, 이번 기회에 직접 경험해보며 내 지식들을 구체화시킬 수 있었다. 개발자가 가볍게 여기고 넘어갈 수 있는 주제들에 대해 다시 생각하며, 기본기의 중요성을 알 수 있었다! 그리고, 기존에 프로젝트하며 고민했던 지점이 있었다. 추상화를 할 때 ‘같은 라인 수를 가져도 굳이 해아하나?’ 생각했었는데, 나한테 해답이 되는 1주차였던 것 같다!! 전부터 ‘한 번 들어야지!’했던 강의인데 워밍업 스터디를 계기로 수강하길 잘한 것 같다🙂 📌 1주차 미션Day 2 : 추상과 구체 예시 생각해보기뭔가 추상화를 실생활에 생각해본 적은 없었는데, 느낌이 새로웠다. 되게 개발스러운 단어라고만 생각했는데 추상은 우리의 삶에도 가까이 적용되는 단어라는 것을 느꼈다.농구에서 슛을 한다고 해보자. 이때 무릎을 구부리고, 팔꿈치를 올리고, 무릎을 다시 피고, 팔꿈치를 다시 피고, 손목을 피며 공을 던진다. 위 여러 과정들이 ‘슛’으로 추상화된다. 우리 삶의 추상화는 상당히 자주 쓰인다!Day 4 : [섹션 3. 논리, 사고의 흐름] 내용을 중심으로 리팩토링하기이전에는 코드를 리팩토링하면, 되게 느낌적으로 다가갔던 것 같다. ‘뭔가 이게 나은 것 같긴한데..’하며 접근하거나 단순히 코드 라인을 줄이는 등 이유와 근거가 빈약했다. 하지만 Day 4 미션을 통해 리팩토링의 이유와 근거를 통해 리팩토링을 진행해보았다.필요한 정보를 빼내 추상화부정어구 최소화케이스 확인위 과정을 통해 미션에서 주어진 코드를 읽기 좋게 개선하였다. 읽으면서 사고의 흐름이 멈칫하거나 꼬일 수 있던 코드를 바로 읽어나갈 수 있게 작성하려고 노력했다.강의를 들으면서 내가 납득됐던 내용에 대해 직접 리팩토링에 적용하여 ‘읽기 좋은 코드’를 만들어 나갈 수 있었다. 단순히 불필요한 작업을 없애는 것만이 리팩토링이 아니라, ‘읽기 좋은 코드’를 만드는 것도 중요한 리팩토링이라는 것을 깨달았다!

백엔드추상객체지향

예은

[인프런 워밍업 클럽 3기] PM/PO 1주차

김민우 튜터님의 <시작하는 PM/PO들에게 알려주고 싶은, 프로덕트의 모든 것>을 수강하며 인프런 워밍업 클럽에 참여하고 있다.2기에 이어 3기에도 참여 중인데, 이번에도 완주까지 힘내보자.배움프로덕트 매니저란비즈니스에서 필요한 성과를 이해하고 회사의 전략에 맞춰 방향성을 제시하는 사람고객에게도 가치를 제공하고, 우리 사업에도 도움이 되는 프로덕트를 만드는 사람   2. 성공적인 프로덕트의 조건ValuableUsableReasibleViable 3. 프로덕트 마켓 핏(PMF)을 찾은 이후 제품 조직이 하는 일Feature WorkGrowth WorkPMF ExpansionScaling Work (주로 프로덕트 리더가 신경 씀)4. 문제 정의 문제를 종•횡으로 오르내리며 Problem Space, Solution Space를 충분히 탐색할 것우리가 정의한 문제는 의식하지 못한 상위 문제의 솔루션을 내포할 수 있음그 문제는 어떤 점 때문에 중요한가?그 문제는 정말로 중요한가? (기회비용 관점에서 사고)현상 아래 원인은 무엇인가?  5. 이터레이션(Iteration) 제품 제작 -> 테스트 -> 학습 -> … 반복하며 발전피드백 수집 및 개선  6. 어떤 문제가 해결할 가치 있는 문제일까?사람들이 적극적으로 해결책을 찾아 보았는가? (불편하다, 그런 제품이 있으면 이용하겠다 X) 사람들이 문제 해결을 위해 시간과 돈을 쓰는가? 타당한 비즈니스 모델을 만들 수 있는 문제회고스스로 칭찬하고 싶은 점개인적으로 새로운 환경에 적응하느라 공부를 병행하는 게 조금 어려웠지만 어떻게든 해냈다. 아쉬웠던 점아직까지 학습이 습관화되지 않은 것 같다. 개선이 필요하다보완하고 싶은 점계획을 세워 여유롭게 학습할 수 있는 환경을 조성하고 싶다.다음주에는나만의 규칙을 만들어 일정한 시간에 학습할 것이다.단순히 멘토님의 강의만을 활용하는 것에서 더 나아가, 스스로 찾아보고 공부하며 깊이 있게 학습할 것이다. 

기획 · PM· POPMPO기획프로덕트

채널톡 아이콘