포지션 개요

N타워 백엔드 개발자
온보딩 가이드

Java/Spring Boot 백엔드 + React 프론트엔드 풀스택 포지션. 이 문서는 실무 즉시 투입 가능한 수준까지 빠르게 올리기 위한 셀프 온보딩 종합 가이드입니다.

5+
요구 경력 (년)
4
핵심 기술 스택
2W
목표 준비 기간
계약직
고용형태
📍 근무지

경기 성남시 분당구 정자일로 190 (정자동) N타워
정자역 도보권

🎯 포지션 레벨

중급: 풀스택 API 연동, MyBatis/Oracle
고급: React/Spring Boot 아키텍트

핵심 요구 기술 스택

카테고리기술요구 수준비고
백엔드Java / Spring Boot5년+ 또는 동급Architect 레벨 우대
프론트React (FE 연동)중급 이상async/await, API 연동
DBMyBatis + Oracle실무 필수동적 SQL, 데이터 액세스
인프라Redis / 메시지 큐연동 경험외부 API 포함
💡
JD에서 중급/고급 요건을 병기하고 있어 실제로는 아키텍처 설계 능력까지 보유한 시니어급을 원하는 포지션입니다. 단순 구현이 아닌 설계 관점에서의 경험을 면접에서 강조하세요.

이 문서 활용 방법

1
입사 전 체크리스트 완료
왼쪽 메뉴 "입사 전 체크리스트"에서 준비 항목을 하나씩 체크하며 진행률을 확인하세요.
2
기술 섹션별 코드 예제 실습
Spring Boot, MyBatis/Oracle, Redis, React 섹션에서 코드를 직접 복사해 로컬에서 실행해보세요.
3
업무 프로세스 숙지
Git 전략, 코드리뷰 기준, 트러블슈팅 패턴을 미리 내면화해두세요.
4
기술 면접 Q&A 반복 리뷰
면접에서 자주 나오는 질문과 모범 답변을 구두로 말할 수 있을 때까지 반복하세요.
입사 전 체크리스트

준비 완료 확인

항목을 클릭해 체크하면 진행률이 자동으로 업데이트됩니다. 브라우저를 닫아도 상태가 유지됩니다.

🔴 최우선 (D-3 이전)
🟡 기술 세팅 (D-7 이전)
🟢 업무 프로세스 (D-10 이전)
🔵 면접 준비 (D-5 이전)
기술 세팅

개발환경 구성

Oracle XE, Redis, Spring Boot, React를 로컬에서 한 번에 띄우는 Docker Compose 기반 환경 가이드입니다.

⚠️
Oracle XE 이미지는 용량이 크므로 최초 pull에 시간이 걸립니다. 미리 docker pull gvenzl/oracle-xe:21-slim 를 실행해두세요.

docker-compose.yml — 전체 스택

# docker-compose.ymlversion: '3.9'

services:
  oracle:
    image: gvenzl/oracle-xe:21-slim
    environment:
      ORACLE_PASSWORD: oracle123
      APP_USER: appuser
      APP_USER_PASSWORD: apppass
    ports:
      - "1521:1521"
    volumes:
      - oracle-data:/opt/oracle/oradata
    healthcheck:
      test: ["CMD", "healthcheck.sh"]
      interval: 30s
      timeout: 10s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes
    volumes:
      - redis-data:/data

  backend:
    build: ./backend
    ports:
      - "8080:8080"
    environment:
      SPRING_DATASOURCE_URL: jdbc:oracle:thin:@oracle:1521/XEPDB1
      SPRING_DATASOURCE_USERNAME: appuser
      SPRING_DATASOURCE_PASSWORD: apppass
      SPRING_REDIS_HOST: redis
    depends_on:
      oracle:
        condition: service_healthy
      redis:
        condition: service_started

  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    depends_on:
      - backend

volumes:
  oracle-data:
  redis-data:

Spring Boot application.yml

# src/main/resources/application.ymlspring:
  datasource:
    url: jdbc:oracle:thin:@localhost:1521/XEPDB1
    username: appuser
    password: apppass
    driver-class-name: oracle.jdbc.OracleDriver
    hikari:
      maximum-pool-size: 10
      connection-timeout: 30000
      idle-timeout: 600000

  data:
    redis:
      host: localhost
      port: 6379
      timeout: 3000

  cache:
    type: redis
    redis:
      time-to-live: 300000   # 5분

mybatis:
  mapper-locations: classpath:mapper/**/\*.xml
  configuration:
    map-underscore-to-camel-case: true
    default-fetch-size: 100
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

server:
  port: 8080

logging:
  level:
    com.yourcompany.mapper: DEBUG  # MyBatis SQL 로그

Vite 프록시 설정 (React)

// vite.config.tsimport { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        // /api/users → http://localhost:8080/api/users
      }
    }
  }
})
기술 세팅 · 백엔드

Spring Boot 핵심 패턴

실무에서 반드시 알아야 하는 Spring Boot 3.x 패턴. 각 예제는 복사해서 바로 실행 가능한 수준으로 작성했습니다.

1. 글로벌 예외 처리 — @ControllerAdvice

// 표준 에러 응답 DTO
@Getter
@Builder
public class ErrorResponse {
    private String code;
    private String message;
    private LocalDateTime timestamp;
    private String path;

    public static ErrorResponse of(String code, String message, String path) {
        return ErrorResponse.builder()
            .code(code).message(message).path(path)
            .timestamp(LocalDateTime.now()).build();
    }
}

// 글로벌 예외 핸들러
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusiness(
            BusinessException ex, HttpServletRequest req) {
        return ResponseEntity.status(ex.getStatus())
            .body(ErrorResponse.of(ex.getCode(), ex.getMessage(), req.getRequestURI()));
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
            MethodArgumentNotValidException ex, HttpServletRequest req) {
        String msg = ex.getBindingResult().getFieldErrors().stream()
            .map(f -> f.getField() + ": " + f.getDefaultMessage())
            .collect(Collectors.joining(", "));
        return ResponseEntity.badRequest()
            .body(ErrorResponse.of("VALIDATION_ERROR", msg, req.getRequestURI()));
    }
}

2. Spring Security 6.x + JWT (람다 DSL)

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .cors(cors -> cors.configurationSource(corsSource()))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**", "/actuator/health").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(new JwtAuthFilter(jwtProvider),
                            UsernamePasswordAuthenticationFilter.class)
            .build();
    }

    @Bean
    public CorsConfigurationSource corsSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("http://localhost:3000"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

3. @Async 비동기 처리 + 외부 API 연동

@Service
public class ExternalApiService {

    private final WebClient webClient;

    public ExternalApiService(WebClient.Builder builder) {
        this.webClient = builder
            .baseUrl("https://api.external.com")
            .defaultHeader("Accept", "application/json")
            .build();
    }

    // 동기 호출 — 응답을 기다려야 할 때
    public ExternalData fetchData(String id) {
        return webClient.get()
            .uri("/data/{id}", id)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError,
                res -> Mono.error(new BusinessException("EXTERNAL_API_ERROR")))
            .bodyToMono(ExternalData.class)
            .timeout(Duration.ofSeconds(5))
            .block();
    }

    // 비동기 병렬 호출 — 여러 API를 동시에 호출
    @Async
    public CompletableFuture<ExternalData> fetchDataAsync(String id) {
        ExternalData data = fetchData(id);
        return CompletableFuture.completedFuture(data);
    }
}
면접 포인트: @Async를 사용하려면 @EnableAsync를 설정 클래스에 추가하고, 같은 클래스 내에서 자기 자신의 @Async 메서드를 호출하면 프록시를 우회해 동작하지 않는다는 점을 설명할 수 있어야 합니다.
기술 세팅 · 데이터베이스

MyBatis + Oracle 실무 패턴

JD에서 직접 명시한 기술. Oracle 방언과 MyBatis 동적 SQL 패턴을 코드로 완벽히 숙지해야 합니다.

⚠️
Oracle은 MySQL/PostgreSQL과 다른 방언을 씁니다. ROWNUM, NVL, DECODE, CONNECT BY, SYSDATE, DUAL — 이 키워드들을 면접에서 자연스럽게 쓸 수 있어야 합니다.

1. Mapper 인터페이스 + XML

// UserMapper.java@Mapper
public interface UserMapper {
    List<UserDto> findByCondition(UserSearchParam param);
    Optional<UserDto> findById(Long id);
    void insert(UserDto user);
    void update(UserDto user);
    void delete(Long id);
    int countByCondition(UserSearchParam param);
}
<!-- mapper/UserMapper.xml --><mapper namespace="com.example.mapper.UserMapper">

  <resultMap id="UserResultMap" type="UserDto">
    <id     property="id"        column="USER_ID"/>
    <result property="name"      column="USER_NM"/>
    <result property="email"     column="EMAIL_ADDR"/>
    <result property="status"    column="STATUS_CD"/>
    <result property="createdAt" column="REG_DT"/>
    <!-- 연관 객체 -->
    <association property="dept" javaType="DeptDto">
      <result property="deptId"   column="DEPT_ID"/>
      <result property="deptName" column="DEPT_NM"/>
    </association>
  </resultMap>

  <!-- Oracle 페이징: ROW_NUMBER() OVER 권장 (ROWNUM은 서브쿼리 필수) -->
  <select id="findByCondition" resultMap="UserResultMap">
    SELECT *
    FROM (
      SELECT A.*, ROW_NUMBER() OVER (ORDER BY A.REG_DT DESC) AS RNUM
      FROM (
        SELECT U.USER_ID, U.USER_NM, U.EMAIL_ADDR, U.STATUS_CD, U.REG_DT,
               D.DEPT_ID, D.DEPT_NM
        FROM   TB_USER U
        LEFT   JOIN TB_DEPT D ON D.DEPT_ID = U.DEPT_ID
        <where>
          <if test="name != null and name != ''">
            AND U.USER_NM LIKE '%' || #{name} || '%'
          </if>
          <if test="status != null">
            AND U.STATUS_CD = #{status}
          </if>
          <if test="fromDate != null">
            AND U.REG_DT >= TO_DATE(#{fromDate}, 'YYYY-MM-DD')
          </if>
        </where>
      ) A
    )
    WHERE RNUM BETWEEN #{offset} + 1 AND #{offset} + #{limit}
  </select>

  <!-- 동적 UPDATE (변경된 컬럼만) -->
  <update id="update">
    UPDATE TB_USER
    <set>
      <if test="name != null">USER_NM = #{name},</if>
      <if test="email != null">EMAIL_ADDR = #{email},</if>
      UPD_DT = SYSDATE
    </set>
    WHERE USER_ID = #{id}
  </update>

  <!-- Oracle Sequence로 PK 채번 -->
  <insert id="insert">
    <selectKey keyProperty="id" resultType="long" order="BEFORE">
      SELECT SEQ_USER.NEXTVAL FROM DUAL
    </selectKey>
    INSERT INTO TB_USER (USER_ID, USER_NM, EMAIL_ADDR, STATUS_CD, REG_DT)
    VALUES (#{id}, #{name}, #{email}, #{status}, SYSDATE)
  </insert>

</mapper>

2. Oracle 필수 방언 — 면접 대비

패턴Oracle 문법MySQL 대비 차이
현재 날짜SYSDATEMySQL: NOW()
NULL 대체NVL(col, '기본값')MySQL: IFNULL
조건 변환DECODE(col, '1', 'Y', 'N')MySQL: IF/CASE
페이징ROW_NUMBER() OVER(ORDER BY col)MySQL: LIMIT OFFSET
계층쿼리CONNECT BY PRIORMySQL: 재귀 CTE
문자 합치기col1 || col2MySQL: CONCAT()
더미 테이블SELECT 1 FROM DUALMySQL: 불필요

3. CONNECT BY — 계층형 쿼리

-- 조직도 계층 조회 (부서 트리)
SELECT
    LEVEL,
    LPAD(' ', (LEVEL-1) * 4) || DEPT_NM AS DEPT_NM_TREE,
    DEPT_ID,
    PARENT_DEPT_ID,
    SYS_CONNECT_BY_PATH(DEPT_NM, '>') AS FULL_PATH
FROM   TB_DEPT
START WITH  PARENT_DEPT_ID IS NULL        -- 최상위 노드
CONNECT BY PRIOR DEPT_ID = PARENT_DEPT_ID  -- 부모→자식 방향
ORDER SIBLINGS BY SORT_ORD;
기술 세팅 · 인프라

Redis / 메시지 큐

JD 명시 항목. 캐싱 전략, TTL 관리, Kafka 기본 연동까지 실무 패턴을 코드로 확인합니다.

1. Redis @Cacheable 캐싱 전략

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))
            .disableCachingNullValues()
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));

        // 캐시별 TTL 다르게 설정
        Map<String, RedisCacheConfiguration> configs = Map.of(
            "users",    defaultConfig.entryTtl(Duration.ofMinutes(5)),
            "products", defaultConfig.entryTtl(Duration.ofHours(1)),
            "codes",    defaultConfig.entryTtl(Duration.ofHours(24))
        );
        return RedisCacheManager.builder(factory)
            .cacheDefaults(defaultConfig)
            .withInitialCacheConfigurations(configs)
            .build();
    }
}

@Service
public class UserService {

    @Cacheable(value = "users", key = "#userId")
    public UserDto findById(Long userId) {
        return userMapper.findById(userId).orElseThrow(
            () -> new BusinessException("USER_NOT_FOUND"));
    }

    @CacheEvict(value = "users", key = "#user.id")
    public void update(UserDto user) {
        userMapper.update(user);
    }

    // 특정 조건에만 캐싱
    @Cacheable(value = "products", key = "#id",
               condition = "#id > 0", unless = "#result == null")
    public ProductDto getProduct(Long id) { ... }
}

2. Kafka Producer / Consumer

// Producer
@Service
public class OrderEventProducer {

    private final KafkaTemplate<String, OrderEvent> kafkaTemplate;

    public void sendOrderEvent(OrderEvent event) {
        kafkaTemplate.send("order-events", event.getOrderId().toString(), event)
            .whenComplete((result, ex) -> {
                if (ex != null) {
                    log.error("Kafka 전송 실패: {}", ex.getMessage());
                } else {
                    log.info("Kafka 전송 성공: partition={}, offset={}",
                        result.getRecordMetadata().partition(),
                        result.getRecordMetadata().offset());
                }
            });
    }
}

// Consumer
@Component
public class OrderEventConsumer {

    @KafkaListener(topics = "order-events", groupId = "order-service")
    public void consume(ConsumerRecord<String, OrderEvent> record) {
        log.info("수신: key={}, value={}", record.key(), record.value());
        try {
            processOrder(record.value());
        } catch (Exception e) {
            // DLQ(Dead Letter Queue)로 이동 처리
            log.error("처리 실패 → DLQ 이동: {}", e.getMessage());
        }
    }
}
캐시 무효화 전략 면접 답변: Write-Through(쓸 때 캐시도 같이 갱신) vs Cache-Aside(읽을 때만 캐시 체크) vs Write-Behind(비동기 캐시 후 DB 지연 저장). 일반적으로는 Cache-Aside(@Cacheable + @CacheEvict)를 가장 많이 씁니다.
기술 세팅 · 프론트엔드

React 비동기 패턴

JD에서 직접 명시한 "Promise/async-await 이해" 요건. FE-BE 연동 전체 흐름을 패턴으로 숙지합니다.

1. Axios 인스턴스 + 글로벌 인터셉터

// src/lib/api.tsimport axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'

export const api = axios.create({
  baseURL: '/api',           // Vite proxy → localhost:8080
  timeout: 10000,
  headers: { 'Content-Type': 'application/json' }
})

// 요청 인터셉터 — JWT 토큰 자동 첨부
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  const token = localStorage.getItem('accessToken')
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})

// 응답 인터셉터 — 토큰 만료 시 자동 갱신
api.interceptors.response.use(
  res => res,
  async (error: AxiosError) => {
    const originalRequest = error.config as any
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true
      try {
        const { data } = await api.post('/auth/refresh')
        localStorage.setItem('accessToken', data.accessToken)
        originalRequest.headers.Authorization = `Bearer ${data.accessToken}`
        return api(originalRequest)
      } catch {
        localStorage.clear()
        window.location.href = '/login'
      }
    }
    return Promise.reject(error)
  }
)

2. Custom Hook — 비동기 상태 관리

// src/hooks/useAsync.tsimport { useState, useCallback } from 'react'

interface AsyncState<T> {
  data: T | null
  loading: boolean
  error: Error | null
}

export function useAsync<T>(asyncFn: (...args: any[]) => Promise<T>) {
  const [state, setState] = useState<AsyncState<T>>({
    data: null, loading: false, error: null
  })

  const execute = useCallback(async (...args: any[]) => {
    setState(s => ({ ...s, loading: true, error: null }))
    try {
      const data = await asyncFn(...args)
      setState({ data, loading: false, error: null })
      return data
    } catch (err) {
      setState(s => ({ ...s, loading: false, error: err as Error }))
      throw err
    }
  }, [asyncFn])

  return { ...state, execute }
}

// 사용 예시
const { data: users, loading, error, execute: fetchUsers } = useAsync(
  () => api.get('/users').then(r => r.data)
)

3. Promise.all 병렬 호출

// 여러 API를 병렬로 호출하고 모두 완료된 후 처리
const loadDashboardData = async () => {
  try {
    const [users, products, orders] = await Promise.all([
      api.get('/users'),
      api.get('/products'),
      api.get('/orders?status=pending')
    ])
    setDashboard({ users: users.data, products: products.data, orders: orders.data })
  } catch (err) {
    // 하나라도 실패하면 catch로 빠짐
    console.error('대시보드 로드 실패:', err)
  }
}

// 실패해도 계속 진행 — allSettled
const results = await Promise.allSettled([
  api.get('/optional-data-1'),
  api.get('/optional-data-2')
])
results.forEach(r => {
  if (r.status === 'fulfilled') console.log(r.value.data)
  else console.warn('부분 실패:', r.reason)
})
업무 프로세스

개발 워크플로우

기능 개발부터 배포까지 일반적인 계약직 포지션에서의 업무 흐름입니다.

1주차 합류 액션 플랜

D1
환경 세팅 + 코드베이스 파악
개발 환경 세팅, 기존 코드 구조 파악, 팀 컨벤션(네이밍, 패키지 구조) 확인. 질문 목록 적어두기.
D2
기존 기능 디버깅 + DB 스키마 파악
주요 API 흐름을 디버거로 따라가며 이해. Oracle 스키마(테이블, 인덱스, 시퀀스) 파악. ERD 있으면 반드시 확인.
D3~5
첫 기여 — 작은 기능 or 버그픽스
큰 기능보다 작은 PR로 팀 프로세스(브랜치, 리뷰, 배포) 전체를 한 번 경험해두기.

일반 기능 개발 프로세스

단계내용산출물
요구분석기획서/이슈 확인, 질문 사항 정리질문 리스트, 확인된 스펙
설계API 설계 (URL, 파라미터, 응답), DB 변경 여부API 명세 초안
개발feature 브랜치 → 구현 → 로컬 테스트코드 + 단위테스트
리뷰PR 올리기 → 코드리뷰 → 수정리뷰 완료된 PR
배포develop merge → QA → main merge → 운영 배포배포 완료

꼭 물어봐야 할 것들 (합류 직후)

배포 주기가 어떻게 되나요? 핫픽스 프로세스가 있나요? PR은 누구에게 올리나요? 테스트 환경이 몇 개인가요? 모니터링 도구가 뭔가요? 온콜 담당이 있나요? 코드 스타일 가이드 문서 있나요?
업무 프로세스

코드리뷰 / Git 전략

Git flow 기반 브랜치 전략과 좋은 PR을 만드는 방법.

Git Flow 브랜치 전략

브랜치역할머지 대상
main운영 배포 기준release 브랜치에서만
develop다음 릴리즈 통합feature 브랜치에서
feature/xxx기능 개발develop으로 PR
release/x.x.x릴리즈 준비main + develop
hotfix/xxx운영 긴급 수정main + develop

커밋 메시지 컨벤션

# 형식: type(scope): 간단한 설명

feat(user): 사용자 목록 페이징 기능 추가
fix(order): 주문 금액 계산 오류 수정
refactor(cache): Redis 캐시 TTL 정책 개선
docs(api): 사용자 API 명세 업데이트
test(user): 사용자 서비스 단위테스트 추가
chore(deps): spring-boot 3.2.1로 업그레이드

# Body: 왜 변경했는지 (선택)
# 기존: NVL 함수 사용 → 성능 이슈 발생
# 변경: COALESCE로 교체하여 인덱스 활용 가능하게 수정

PR 템플릿 (좋은 PR 작성법)

## 변경 내용
사용자 목록 조회 API에 페이징 및 검색 필터 기능 추가

## 변경 이유
데이터 증가로 인해 전체 조회 시 응답시간 5초 이상 발생

## 변경 사항
- UserMapper.xml: ROW_NUMBER() 기반 페이징 쿼리 추가
- UserService: 페이지 파라미터 검증 로직 추가
- Redis 캐시 무효화 처리 (@CacheEvict)

## 테스트
- [x] 로컬 실행 확인
- [x] 단위 테스트 통과
- [ ] QA 환경 배포 (진행 중)

## 스크린샷 (UI 변경이 있는 경우)
업무 프로세스

트러블슈팅 가이드

자주 만나는 문제 유형별 원인과 해결 패턴입니다.

Spring Boot 자주 나오는 이슈

증상원인해결
N+1 쿼리 폭발MyBatis collection 루프에서 추가 조회JOIN 쿼리로 통합 또는 Batch 조회
@Transactional 롤백 안 됨checked 예외, 같은 클래스 호출(프록시 우회)unchecked 예외 사용 또는 self-injection
@Async 동작 안 함@EnableAsync 누락 또는 같은 클래스 호출@EnableAsync 추가, 별도 빈으로 분리
캐시 무효화 안 됨key 표현식 불일치key를 동일한 SpEL 표현식으로 통일
CORS 오류SecurityConfig에서 CORS 설정 누락corsConfigurationSource 빈 등록 확인

Oracle / MyBatis 자주 나오는 이슈

증상원인해결
ORA-01795: 목록이 1000개 초과IN 절에 1000개 이상 바인딩IN 절을 분할(배치)하거나 임시 테이블 사용
ROWNUM 페이징이 틀림ROWNUM을 WHERE에서 직접 사용서브쿼리 감싼 후 RNUM 조건 사용
한글 깨짐JDBC URL characterEncoding 미설정NLS_CHARACTERSET 확인, UTF-8 설정
MyBatis resultMap 매핑 안 됨column명 대소문자 불일치column 속성에 Oracle 컬럼명 그대로(대문자)

Redis 관련

# 캐시 상태 확인
redis-cli KEYS "users::*"       # 특정 캐시 키 목록
redis-cli TTL "users::123"      # TTL 남은 시간(초)
redis-cli GET "users::123"      # 캐시 값 확인
redis-cli FLUSHDB               # 개발환경 전체 캐시 삭제 (주의!)

# 연결 문제
redis-cli ping                  # PONG 응답 확인
redis-cli INFO server           # 서버 정보 확인
면접 준비

기술 면접 Q&A

이 포지션에서 나올 가능성이 높은 질문과 핵심 답변 포인트입니다. 각 항목을 소리 내어 2분 안에 설명할 수 있을 때까지 연습하세요.

Q1. MyBatis N+1 문제란 무엇이고 어떻게 해결하나요? DB

핵심 답변: 1번의 쿼리 결과에서 N개 행이 나올 때, 각 행에 대해 추가 쿼리가 N번 실행되는 문제입니다. 예를 들어 부서 목록 1개를 조회하면서 각 부서의 사원 목록을 루프에서 조회하면 1 + N번 실행됩니다.

해결: ① JOIN 쿼리로 한 번에 조회 + resultMap collection으로 매핑 ② 배치 조회(IN 절) ③ 2차 캐시 활용

Q2. @Transactional propagation REQUIRED와 REQUIRES_NEW의 차이는? Spring

REQUIRED (기본값): 이미 트랜잭션이 있으면 참여, 없으면 새로 시작. 부모-자식이 같은 트랜잭션 → 어느 쪽 예외든 전체 롤백.

REQUIRES_NEW: 항상 새 트랜잭션 시작, 기존 트랜잭션은 일시 중단. 자식 성공/실패가 부모에 독립적. 로그 기록이나 감사 이력처럼 메인 트랜잭션 실패와 무관하게 반드시 저장해야 할 때 사용.

Q3. React에서 useEffect 의존성 배열의 역할은? React

핵심 답변: useEffect가 다시 실행될 조건을 명시합니다. ① 빈 배열([]) → 마운트 시 1회만 실행 ② 값이 있으면 → 해당 값이 변경될 때마다 실행 ③ 생략하면 → 매 렌더링마다 실행 (보통 의도치 않은 버그 유발).

cleanup 함수: return () => {} 로 이벤트 리스너 제거, API 취소(AbortController) 등 사이드이펙트 정리.

Q4. Redis 캐싱을 언제 쓰고 언제 안 쓰는지 판단 기준은? Redis

써야 할 때: 읽기 빈도가 높고 변경이 적은 데이터 (공통코드, 상품 목록, 세션). 계산 비용이 큰 집계 쿼리.

쓰면 안 될 때: 실시간 정합성이 반드시 필요한 잔액/재고. 쓰기 빈도가 읽기보다 높은 경우. 캐시 무효화 로직이 복잡해져 버그 유발 가능성이 높을 때.

Q5. Oracle ROW_NUMBER()와 ROWNUM의 차이는? Oracle

ROWNUM: 쿼리 결과에 순서대로 붙는 의사 컬럼. ORDER BY 이전에 매겨지므로 페이징 시 반드시 서브쿼리로 감싸야 함.

ROW_NUMBER() OVER(ORDER BY ...): 윈도우 함수로, ORDER BY 후 번호를 매기므로 페이징에 훨씬 안전하고 직관적. 실무에서는 ROW_NUMBER() 사용 권장.

-- ROWNUM 방식 (서브쿼리 2중 필수)
SELECT * FROM (SELECT A.*, ROWNUM RN FROM (SELECT * FROM TB WHERE ...) A)
WHERE RN BETWEEN 1 AND 10;

-- ROW_NUMBER 방식 (권장)
SELECT * FROM (SELECT A.*, ROW_NUMBER() OVER(ORDER BY REG_DT) RN FROM TB A)
WHERE RN BETWEEN 1 AND 10;
Q6. Promise.all과 Promise.allSettled의 차이는? JS

Promise.all: 하나라도 reject되면 즉시 catch로 빠짐. 모두 성공해야 하는 경우 (주문 처리, 결제).

Promise.allSettled: 모든 Promise가 완료될 때까지 기다림. 각각 fulfilled/rejected 결과를 배열로 반환. 일부 실패해도 나머지 결과를 처리해야 할 때 (대시보드 위젯별 독립 로딩).

💡
모든 답변에는 "실제로 이런 상황에서 이렇게 해결했습니다" 형태의 구체적 경험을 1~2문장 덧붙이면 훨씬 설득력이 높아집니다. 본인의 프로젝트 경험과 연결해서 준비해두세요.