Xcode 내 빌드 오류 (Could not locate device support files, Could not locate Developer Disk Image for this device)
이슈 발생)
회사에서 프로젝트를 진행하면서 QA 확인사항에서 iOS13 버전에 이슈가 생겨서 해당 버전의 아이폰에 앱을 빌드하여 테스트해봐야 하는 상황이 발생했다. 기존에 iOS 앱을 TestFlight 에 배포하거나 그때 그때 빌드하여 테스트를 진행하는 중이었기 때문에 대수롭지 않게 테스트 폰을 맥에 연결하여 빌드하려 하였으나 한번도 보지 못한 위의 이미지와 같은 오류가 발생하며 빌드가 되지 않았다.
오류 내용은 각 오류 케이스에 따라 아래와 같다.
Could not locate device support files 오류
Could not locate device support files You may be able to resolve the issue by installing the latest version of Xcode from the Mac App Store or developer.apple.com.
Could not locate Developer Disk Image for this device 오류
위 두 오류는 구글링 해보니 동일한 오류로 판단되었고, 직역해보자면 지원 파일이 존재하지 않아서 문제가 발생하였고, Xcode 의 최신 버전 설치를 통해 이 문제를 해결할 수 있을지도 모른다는 내용이다.
해결 시도)
2025.10.27 기준 최신 버전 26.0.1
오류 내용에 따라 Xcode 업데이트를 최신버전인 26.0.1 버전으로 설치 후 다시 빌드를 시도했으나 동일한 오류가 발생하였다.
위 내용으로 다시 구글링을 하다 보니 Xcode 업데이트를 통한 해결은 iOS 버전이 Xcode 의 iOS 지원버전보다 높은 경우에 사용할 수 있는 해결방법이었다.
즉, 내 Xcode 버전이 구버전이고 빌드하려는 iOS 버전이 최신인 경우에만 해당되는 해결 방법이었던 것이다.
위 내용을 확인한 뒤 Xcode 의 지원 iOS 를 확인보았다.
https://developer.apple.com/kr/support/xcode/
이미 내 Xcode 버전에서는 iOS 15 버전 아래로는 정식 지원이 되지 않는 상태였다...
다시 구글링을 진행해보니 Xcode 에서는 아래와 같은 패키지 경로 내에 DeviceSupport 파일을 관리하고 있으며 설치를 원하는 버전에 맞는 DeviceSupport 파일을 저기에 넣어주면 정상적으로 설치가 가능하다는 글을 보았다.
우선, 결과부터 얘기해보자면 아래에 기술하는 첫번째의 방법으로는 구현을 실패하였고 차선책으로 사용한 두번째 구현 방법으로 구현에 성공하였다.
1️⃣ 순수 자바코드로 구현 시도
해당 오픈소스 사용 방법으로는 해당 오픈소스 의존성을 추가하고 자바 코드로 노드를 시각화하려면 아래와 같이 가변형 그래프객체(그래프에 노드를 추가하는 형식이 아니라 한번에 그래프를 생성하는 경우는 MutableGraph 객체를 사용하지 않아도 된다)를 생성하고 가변형 노드객체를 선언하여 추가하는 형식으로 이용하면 된다.
import guru.nidi.graphviz.attribute.Color;
import guru.nidi.graphviz.engine.Format;
import guru.nidi.graphviz.engine.Graphviz;
import guru.nidi.graphviz.model.MutableGraph;
import guru.nidi.graphviz.model.MutableNode;
import static guru.nidi.graphviz.model.Factory.*;
import java.io.File;
import java.io.IOException;
public class GraphExample {
public static void main(String[] args) {
try {
// MutableGraph 객체 생성
MutableGraph g = mutGraph("example1").setDirected(true);
// 개별 노드 생성
MutableNode nodeA = mutNode("a").add(Color.RED);
MutableNode nodeB = mutNode("b");
// 노드를 그래프에 추가
g.add(nodeA);
g.add(nodeB);
// 노드 간 링크 추가
nodeA.addLink(nodeB);
// 그래프를 PNG로 렌더링 후 파일로 저장
Graphviz.fromGraph(g).width(200).render(Format.PNG).toFile(new File("example/ex1m.png"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
( JAVA 코드로 객체화하여 그래프를 생성하는 방법 )
여기서 각 객체를 선언 및 사용하는 부분에서는 Graphviz 모듈을 사용하지 않지만 .render() 메소드 부분에서는 직접적으로 Graphviz 의 모듈 중 하나인 dot 명령어를 사용하므로 Graphviz 를 프로젝트가 돌아갈 운영체제에 설치한다.
⬇️ MAC, Linux Graphviz 설치 방법 ⬇️
# ⬇️ MAC 설치 ⬇️
brew install graphviz
# ⬇️ Linux 설치 (Debian 계열 - Ubuntu) ⬇️
sudo apt install graphviz
# ⬇️ Linux 설치 (Redhat 계열) ⬇️
sudo dnf install graphviz
그리고 코드를 실행시켜주기만 하면 되는데 ( dot is not defined ) 오류가 이클립스의 콘솔에서 발생하였고 Graphviz 를 설치했음에도 이클립스에서 프로젝트를 실행할 경우 내장된 dot 명령어를 찾지 못해 환경변수 설정까지 진행을 시켜봤지만 동일한 오류가 계속 발생하였다.
( 이클립스 프로그램 내의 환경변수 설정도 지정해줬지만 동일하게 dot 명령어를 찾지 못했다. )
혹시 몰라 터미널에서 dot -V 명령어를 통해 dot 명령어가 정상적으로 실행되는지 확인해보았지만 터미널에서는 정상작동하는 것을 볼 수 있었다.
그래서 다음 방법을 찾아보다가 아래와 같은 방법으로 구현을 진행하게 되었다.
2️⃣ 자바에서 shell 명령어를 사용하여 구현
public class GraphExample {
public static void main(String[] args) {
ArrayList<String> routerTypeList = new ArrayList<>();
ArrayList<String> currentPathList = new ArrayList<>();
String command = "export PATH=\"$PATH:/usr/local/bin\"; echo 'digraph {";
try {
for (int i = 0; i < 앱이동경로MapList.size(); i++) {
routerTypeList.add(앱이동경로MapList.routerType);
// 쉘에서는 경로를 나타내는 / 를 사용하면 오류를 뱉기 때문에
// 이동경로(ex. /home) 과 같은 \ 를 추가하여 이스케이프 문자로 처리함
if(앱이동경로MapList.currentPath.charAt(0) == '\\') {
currentPathList.add('\"' + 앱이동경로MapList.currentPath + '\"');
}else {
currentPathList.add('\"' + 앱이동경로MapList.currentPath + '\"');
}
}
for(int i=0; i<currentPathList.size(); i++) {
if(i==0) {
// 앱 내 이동경로가 1개인 경우 경로를 추가하지 않고 node 하나만 출력하기 위해
// 커맨드에 해당 노드 경로만 추가
if(currentPathList.size() == 1) {
command += currentPathList.get(0);
}
continue;
}else {
// 만약 커맨드 내에 노드 -> 노드(ex. a -> b) 이동이 이미 중복 선언되어 있다면
// 중복 이동 그래프를 그리는 것을 방지하기 위해 커맨드를 추가하지 않음
// 이 부분을 제거하면 모든 중복 경로이동의 화살표가 표현됨
if(!command.contains(currentPathList.get(i-1) + "->" + currentPathList.get(i) + " ")) {
command += (currentPathList.get(i-1) + "->" + currentPathList.get(i) + " ");
}
}
}
// svg 파일을 생성할때 사용할 랜덤한 uuid 를 생성
// API 요청은 비동기로 작동하기 때문에 중복 요청의 경우의 수를 생각
String uuid = UUID.randomUUID().toString();
// sh shell 로 전송할 커맨드를 작성
// -Tsvg 옵션으로 svg 파일로 그래프를 생성
// -Nshape=rect 로 모든 노드를 사각형으로 표현
// -o {파일명} 을 통해 파일의 이름 선언
command += "}' | dot -Tsvg -Nshape=rect -o " + uuid + ".svg;";
// 명령어를 실행할 ProcessBuilder 객체를 생성
ProcessBuilder builder = new ProcessBuilder();
String homeDirectory = System.getProperty("user.dir");
log.debug("##### homeDirectory ::: {}", homeDirectory);
File graphDir = new File(homeDirectory, "graph");
if(!graphDir.exists()) {
if(!graphDir.mkdir()) {
log.error("##### fail to mkdir ::: {}", graphDir.getAbsolutePath());
}
}
builder.directory(graphDir);
// sh 쉘로 커맨드 실행
builder.command("sh", "-c", command);
Process process = builder.start();
// 에러 발생 시 커스텀된 StreamGobbler 를 사용하여 sh shell 내의 에러를 읽어
// 콘솔에 출력함
StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), System.err::println);
Executors.newSingleThreadExecutor().submit(errorGobbler);
log.debug("##### success make graph ::: {}/{}.svg", graphDir.getAbsolutePath(), uuid);
int exitCode = process.waitFor();
assert exitCode == 0;
// 생성된 파일을 읽어와 Base64 로 인코딩하여 문자열로 변환한 뒤 응답
File graphFile = new File(graphDir, uuid + ".svg");
if(graphFile.exists()) {
try {
byte[] graphContent = Files.readAllBytes(graphFile.toPath());
String encodedGraph = Base64.getEncoder().encodeToString(graphContent);
message = encodedGraph;
}catch(IOException e) {
message = "";
e.printStackTrace();
}
}else {
message = "";
}
// 생성된 svg 파일을 삭제 및 삭제되지 않은 경우 log 출력
if(!graphFile.delete()) {
log.debug("##### fail to delete graphFile ::: {}", graphFile.getAbsolutePath());
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
message = e.getMessage();
} finally {
}
return returnJson(message);
}
}
( 그래프 생성 코드 )
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.function.Consumer;
public class StreamGobbler implements Runnable {
private InputStream inputStream;
private Consumer<String> consumer;
public StreamGobbler(InputStream inputStream, Consumer<String> consumer) {
this.inputStream = inputStream;
this.consumer = consumer;
}
@Override
public void run() {
new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(consumer);
}
}
attemptTime 컬럼은 이메일 인증을 시도한 시점의 시간을 저장하는 컬럼인데 5분 내 이메일 인증 시도 횟수를 아래와 같이 COUNT 를 사용하여 출력해보려 하였지만 몇 번을 요청하든 0회로 찍혀서 나오는 오류가 발생하였다.
네이티브 쿼리를 활용하여 현재 시간의 5분 내에 특정 이메일로 요청한 데이터의 갯수를 출력하도록 메소드를 작성
혹시 Hibernate 에서 쿼리를 Insert 하는 순간에 attempt_time 컬럼이 비어있는 상태로 넘어가는지 로그를 확인해보았지만 정상적으로 Insert 는 실행되는 모습이다.
정상적으로 attempt_time 컬럼의 값이 채워져서 insert 되는 모습
@CreateTimeStamp 어노테이션을 사용하면 jpa 를 사용하여 save() 메소드를 사용해 객체의 데이터를 데이터베이스에 저장하는 시점에 Hibernate 가 해당 어노테이션을 인식하고 자동으로 값을 채워넣어 주는 형태이기 때문에 save() 메소드를 사용해 저장하는 나는 당연하게도 값이 누락될 리는 없었다.
저장된 값을 확인해보려 MySQL 을 실행하였다.
( 로그로 확인할 수 없는 이유는 save() 메소드 실행 시 값이 채워지기 때문에 생성된 객체의 getter 를 사용해서 데이터를 뽑아도 출력되지 않기 때문 )
id 값이 202 ~ 352 까지가 오늘 요청한 부분
MySQL 을 열어 확인해보니 attempt_time 의 값은 채워지는데 시간대의 설정이 올바르게 되어있지 않는 듯 하였다.
우선적으로 attempt_time 의 값을 채워주는 Hibernate 는 JDBC 의 시간대 설정을 따른다고 검색을 통해 알아냈다.
application.yml 의 url 항목 설정을 확인해보니 serverTimezone 이 UTC(세계 협정 기준시) 로 설정되어 있는 것을 확인할 수 있었다.
JDBC 의 serverTimezone 을 한국의 시간대로 변경
시간대를 서울 기준으로 변경한 뒤 다시 api 요청을 보내보니 정상적인 한국 시간 기준으로 attempt_time 이 저장되는 것을 확인할 수 있었다.
한국 표준시로 정확하게 저장되는 것 확인, API 요청시 인증 횟수 5회 초과 요청시 문구 응답